Skip to content

Instantly share code, notes, and snippets.

@mikehostetler
Created August 25, 2025 12:44
Show Gist options
  • Save mikehostetler/ec30046da20eec655836210aa2079203 to your computer and use it in GitHub Desktop.
Save mikehostetler/ec30046da20eec655836210aa2079203 to your computer and use it in GitHub Desktop.
SPIKE: Jido Calculator Agent
defmodule CalculatorAgent do
@moduledoc """
A simple calculator agent for performing arithmetic operations.
CalculatorAgent provides a clean, minimal API for mathematical calculations
while leveraging the full power of Jido.Agent.Server under the hood.
## Examples
# Start a calculator agent
{:ok, pid} = CalculatorAgent.start_link(name: "my_calculator")
# Simple calculations
{:ok, result} = CalculatorAgent.calculate(pid, "2 + 2")
{:ok, result} = CalculatorAgent.calculate(pid, "sqrt(16)")
# View calculation history
{:ok, history} = CalculatorAgent.history(pid)
"""
use Jido.Agent,
name: "calculator_agent",
description: "Simple calculator agent for arithmetic operations",
category: "Math Agents",
tags: ["calculator", "math", "arithmetic"],
vsn: "1.0.0",
schema: [
# Track calculation history
calculations: [type: {:list, :map}, default: []],
calculation_count: [type: :integer, default: 0]
],
actions: [
# All actions are now automatically registered by skills
]
require Logger
@default_opts [
agent: __MODULE__,
mode: :auto,
log_level: :info,
skills: [
Jido.Skills.Arithmetic,
Jido.Skills.StateManager,
Jido.Skills.BasicActions
]
]
@default_timeout 30_000
@impl true
def start_link(opts) when is_list(opts) do
# Ensure name is provided
name = Keyword.fetch!(opts, :name)
# Set up agent with calculation state
initial_state = %{
calculations: [],
calculation_count: 0
}
# Set up signal routing - StateManager skill handles jido.state.* automatically
routes = []
# Merge default options with routing and skills
server_opts =
@default_opts
|> Keyword.merge(opts)
|> Keyword.put(:id, name)
|> Keyword.put(:initial_state, initial_state)
|> Keyword.put(:routes, routes)
Jido.Agent.Server.start_link(server_opts)
end
@doc """
Primary calculation interface - evaluates mathematical expressions.
## Examples
{:ok, result} = CalculatorAgent.calculate(pid, "2 + 2")
{:ok, result} = CalculatorAgent.calculate(pid, "sqrt(25) * 2")
{:ok, result} = CalculatorAgent.calculate(pid, "sin(pi/2)")
"""
@spec calculate(pid() | String.t(), String.t()) :: {:ok, number()} | {:error, term()}
def calculate(agent_ref, expression) when is_binary(expression) do
with {:ok, pid} <- resolve_pid(agent_ref),
{:ok, signal} <- build_calc_signal(expression) do
case Jido.Agent.Server.call(pid, signal, @default_timeout) do
{:ok, result} ->
# Store calculation in history
store_calculation(pid, expression, result)
{:ok, result}
error ->
error
end
end
end
@doc """
Get the agent's calculation history.
## Examples
{:ok, history} = CalculatorAgent.history(pid)
"""
@spec history(pid() | String.t()) :: {:ok, [map()]} | {:error, term()}
def history(agent_ref) do
with {:ok, pid} <- resolve_pid(agent_ref),
{:ok, state} <- Jido.Agent.Server.state(pid) do
calculations = get_in(state.agent.state, [:calculations]) || []
{:ok, calculations}
end
end
@doc """
Clear the agent's calculation history.
## Examples
:ok = CalculatorAgent.clear(pid)
"""
@spec clear(pid() | String.t()) :: :ok | {:error, term()}
def clear(agent_ref) do
with {:ok, pid} <- resolve_pid(agent_ref),
{:ok, clear_calcs_signal} <- build_clear_calculations_signal(),
{:ok, reset_count_signal} <- build_reset_count_signal() do
with {:ok, _} <- Jido.Agent.Server.call(pid, clear_calcs_signal),
{:ok, _} <- Jido.Agent.Server.call(pid, reset_count_signal) do
:ok
else
error -> error
end
end
end
@doc """
Get the count of calculations performed.
## Examples
{:ok, count} = CalculatorAgent.count(pid)
"""
@spec count(pid() | String.t()) :: {:ok, integer()} | {:error, term()}
def count(agent_ref) do
with {:ok, pid} <- resolve_pid(agent_ref),
{:ok, state} <- Jido.Agent.Server.state(pid) do
count = get_in(state.agent.state, [:calculation_count]) || 0
{:ok, count}
end
end
## Signal Handling
@impl true
def transform_result(%Jido.Signal{type: "arithmetic.result"}, result, _instruction) do
# Extract numeric result from arithmetic evaluation
{:ok, result}
end
def transform_result(%Jido.Signal{type: "arithmetic.eval"}, %{result: result}, _instruction) do
# Extract numeric result directly from Eval action
{:ok, result}
end
def transform_result(%Jido.Signal{type: "jido.state.set"}, _result, _instruction) do
{:ok, "State updated"}
end
def transform_result(%Jido.Signal{type: type}, result, _instruction)
when type in ["jido.state.get", "jido.state.update", "jido.state.delete"] do
{:ok, result}
end
## Private Implementation
defp resolve_pid(pid) when is_pid(pid), do: {:ok, pid}
defp resolve_pid(name) when is_binary(name) do
case Process.whereis(String.to_atom(name)) do
nil -> {:error, {:agent_not_found, name}}
pid -> {:ok, pid}
end
end
defp build_calc_signal(expression) do
Jido.Signal.new(%{
type: "arithmetic.eval",
data: %{expression: expression}
})
end
defp build_clear_calculations_signal do
Jido.Signal.new(%{
type: "jido.state.set",
data: %{path: [:calculations], value: []}
})
end
defp build_reset_count_signal do
Jido.Signal.new(%{
type: "jido.state.set",
data: %{path: [:calculation_count], value: 0}
})
end
defp store_calculation(pid, expression, result) do
# Get current state to increment count and add calculation
with {:ok, state} <- Jido.Agent.Server.state(pid) do
current_count = get_in(state.agent.state, [:calculation_count]) || 0
current_calcs = get_in(state.agent.state, [:calculations]) || []
new_calculation = %{
expression: expression,
result: result,
timestamp: DateTime.utc_now()
}
# Update count
count_signal =
Jido.Signal.new!(%{
type: "jido.state.set",
data: %{path: [:calculation_count], value: current_count + 1}
})
# Update calculations list
calc_signal =
Jido.Signal.new!(%{
type: "jido.state.set",
data: %{path: [:calculations], value: [new_calculation | current_calcs]}
})
# Send both updates (fire and forget)
Jido.Agent.Server.call(pid, count_signal)
Jido.Agent.Server.call(pid, calc_signal)
end
end
end
#!/usr/bin/env elixir
# CalculatorAgent Demo Script
# This script demonstrates the CalculatorAgent with skills defined in default_opts
# Load the project
Mix.install([
{:jido, path: "."}
])
# Load our calculator agent
Code.require_file("lib/agents/calculator_agent.ex")
defmodule CalculatorDemo do
require Logger
def run do
Logger.info("🧮 Starting CalculatorAgent Demo")
Logger.info("======================================")
# Start the calculator agent
{:ok, pid} = CalculatorAgent.start_link(name: :demo_calculator)
Logger.info("✅ Started CalculatorAgent with PID: #{inspect(pid)}")
# Demonstrate basic calculations
Logger.info("\n📊 Testing Basic Calculations:")
test_calculation(pid, "2 + 2")
test_calculation(pid, "10 * 5")
test_calculation(pid, "sqrt(16)")
test_calculation(pid, "sin(pi/2)")
test_calculation(pid, "(3 + 4) * 2")
# Check calculation count
Logger.info("\n📈 Testing Count & History:")
{:ok, count} = CalculatorAgent.count(pid)
Logger.info("Total calculations: #{count}")
# Get calculation history
{:ok, history} = CalculatorAgent.history(pid)
Logger.info("History entries: #{length(history)}")
Enum.with_index(history, 1)
|> Enum.each(fn {calc, index} ->
Logger.info(" #{index}. #{calc.expression} = #{calc.result}")
end)
# Test error handling
Logger.info("\n⚠️ Testing Error Handling:")
test_calculation(pid, "1 / 0")
test_calculation(pid, "invalid_expression")
# Test clearing history
Logger.info("\n🧹 Testing Clear History:")
:ok = CalculatorAgent.clear(pid)
{:ok, new_count} = CalculatorAgent.count(pid)
{:ok, new_history} = CalculatorAgent.history(pid)
Logger.info("After clear - Count: #{new_count}, History: #{length(new_history)} entries")
# Test more calculations after clear
Logger.info("\n🔄 Testing After Clear:")
test_calculation(pid, "100 / 4")
test_calculation(pid, "cos(0)")
{:ok, final_count} = CalculatorAgent.count(pid)
Logger.info("Final count: #{final_count}")
Logger.info("\n✅ Demo completed successfully!")
Logger.info("======================================")
# Stop the agent
GenServer.stop(pid)
:ok
end
defp test_calculation(pid, expression) do
case CalculatorAgent.calculate(pid, expression) do
{:ok, result} ->
Logger.info(" ✅ #{expression} = #{result}")
{:error, reason} ->
Logger.warn(" ❌ #{expression} failed: #{inspect(reason)}")
end
end
end
# Run the demo
CalculatorDemo.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment