diff --git a/lib/jido/agent/server_callback.ex b/lib/jido/agent/server_callback.ex index ebbd863..70b3a21 100644 --- a/lib/jido/agent/server_callback.ex +++ b/lib/jido/agent/server_callback.ex @@ -4,6 +4,7 @@ defmodule Jido.Agent.Server.Callback do use ExDbug, enabled: false alias Jido.Agent.Server.State, as: ServerState alias Jido.Signal + alias Jido.Signal.Router require OK @doc """ @@ -217,35 +218,11 @@ defmodule Jido.Agent.Server.Callback do all_patterns = input_patterns ++ output_patterns Enum.any?(all_patterns, fn pattern -> - pattern_matches?(signal.type, pattern) + Router.matches?(signal.type, pattern) end) end) dbug("Found matching skills", matches: matches) matches end - - # Checks if a signal type matches a pattern using glob-style matching. - # - # Parameters: - # - signal_type: The signal type to check - # - pattern: The pattern to match against (can include * wildcards) - # - # Returns: - # true if the signal type matches the pattern, false otherwise - @spec pattern_matches?(signal_type :: String.t(), pattern :: String.t()) :: boolean() - defp pattern_matches?(signal_type, pattern) do - dbug("Checking pattern match", signal_type: signal_type, pattern: pattern) - - regex = - pattern - |> String.replace(".", "\\.") - |> String.replace("*", "[^.]+") - |> then(&"^#{&1}$") - |> Regex.compile!() - - matches = Regex.match?(regex, signal_type) - dbug("Pattern match result", matches: matches) - matches - end end diff --git a/lib/jido/agent/server_skills.ex b/lib/jido/agent/server_skills.ex index 2b844fc..135ed7f 100644 --- a/lib/jido/agent/server_skills.ex +++ b/lib/jido/agent/server_skills.ex @@ -1,50 +1,123 @@ defmodule Jido.Agent.Server.Skills do - @moduledoc false + @moduledoc """ + Functions for building and managing skills in the agent server. + """ + use ExDbug, enabled: false alias Jido.Agent.Server.State, as: ServerState + alias Jido.Skill + + @doc """ + Builds the skills for the agent server. + + This function takes a list of skills from the options and adds them to the server state. + It also collects any routes and child_specs from the skills and adds them to the options. + + ## Parameters - @doc false - def build(%ServerState{} = state, opts) do + - `state` - The current server state + - `opts` - The options for the server + + ## Returns + + - `{:ok, state, opts}` - The updated state and options + - `{:error, reason}` - An error occurred + """ + @spec build(ServerState.t(), Keyword.t()) :: {:ok, ServerState.t(), Keyword.t()} | {:error, String.t()} + def build(state, opts) do dbug("Building skills", state: state, opts: opts) - case opts[:skills] do + case Keyword.get(opts, :skills) do nil -> dbug("No skills configured") {:ok, state, opts} skills when is_list(skills) -> dbug("Processing skills list", skills: skills) + build_skills(state, skills, opts) + + invalid -> + dbug("Invalid skills configuration", invalid: invalid) + {:error, "Skills must be a list, got: #{inspect(invalid)}"} + end + end + + defp build_skills(state, skills, opts) do + # Initialize accumulators + init_acc = {state, opts, [], []} - skills - |> Enum.reduce_while({:ok, state, opts}, fn skill, {:ok, acc_state, acc_opts} -> - dbug("Processing skill", skill: skill) + # Process each skill + case Enum.reduce_while(skills, init_acc, &process_skill/2) do + {:error, reason} -> + {:error, reason} - # Add skill module to state - updated_state = %{acc_state | skills: [skill | acc_state.skills]} - dbug("Added skill to state", updated_state: updated_state) + {updated_state, updated_opts, routes, child_specs} -> + # Merge routes and child_specs into options + final_opts = + updated_opts + |> Keyword.update(:routes, routes, &(&1 ++ routes)) + |> Keyword.update(:child_specs, child_specs, &(child_specs ++ &1)) - # Get routes and child_specs from skill - skill_routes = skill.routes() - skill_child_specs = skill.child_spec([]) + dbug("Updated options with skill config", updated_opts: final_opts) + {:ok, updated_state, final_opts} + end + end - dbug("Got skill routes and child specs", - skill_routes: skill_routes, - skill_child_specs: skill_child_specs - ) + defp process_skill(skill, {state, opts, routes_acc, child_specs_acc}) do + dbug("Processing skill", skill: skill) - # Merge routes and child_specs into opts - updated_opts = - acc_opts - |> Keyword.update(:routes, skill_routes, &(&1 ++ skill_routes)) - |> Keyword.update(:child_specs, [skill_child_specs], &[skill_child_specs | &1]) + # Get the skill's opts_key + opts_key = skill.opts_key() + dbug("Skill opts_key", opts_key: opts_key) - dbug("Updated options with skill config", updated_opts: updated_opts) - {:cont, {:ok, updated_state, updated_opts}} - end) + # Get the options for this skill from the main opts, defaulting to empty keyword list + skill_opts = Keyword.get(opts, opts_key, []) + dbug("Skill options", skill_opts: skill_opts) - invalid -> - dbug("Invalid skills configuration", invalid: invalid) - {:error, "Skills must be a list, got: #{inspect(invalid)}"} + # Validate the skill options against the skill's schema + case Skill.validate_opts(skill, skill_opts) do + {:ok, validated_opts} -> + dbug("Skill options validated successfully", validated_opts: validated_opts) + + # Update the agent's state with the validated options + updated_agent = + Map.update!(state.agent, :state, fn current_state -> + Map.put(current_state, opts_key, validated_opts) + end) + + # Call the skill's mount callback to allow it to transform the agent + case skill.mount(updated_agent, validated_opts) do + {:ok, mounted_agent} -> + dbug("Mounted skill to agent", mounted_agent: mounted_agent) + + # Update the state with the skill and mounted agent + updated_state = %{state | skills: [skill | state.skills], agent: mounted_agent} + dbug("Updated agent state with skill options", updated_state: updated_state) + + # Get routes from the skill's router function + new_routes = skill.router(validated_opts) + dbug("Got skill routes", routes: new_routes) + + # Validate routes + if is_list(new_routes) do + # Get child_spec from the skill + new_child_specs = [skill.child_spec(validated_opts)] + dbug("Got skill child specs", child_specs: new_child_specs) + + # Continue processing with updated accumulators + {:cont, {updated_state, opts, routes_acc ++ new_routes, child_specs_acc ++ new_child_specs}} + else + {:halt, {:error, "Skill #{skill.name()} returned invalid routes: #{inspect(new_routes)}"}} + end + + {:error, reason} -> + dbug("Skill mount failed", reason: reason) + {:halt, {:error, "Failed to mount skill #{skill.name()}: #{inspect(reason)}"}} + end + + {:error, reason} -> + dbug("Skill options validation failed", reason: reason) + {:halt, {:error, "Failed to validate options for skill #{skill.name()}: #{inspect(reason)}"}} end end end diff --git a/lib/jido/skill.ex b/lib/jido/skill.ex index 6d147a0..4a9126c 100644 --- a/lib/jido/skill.ex +++ b/lib/jido/skill.ex @@ -22,8 +22,7 @@ defmodule Jido.Skill do Skills use schema-based state isolation to prevent different capabilities from interfering with each other. Each skill defines: - - A unique `schema_key` for namespace isolation - - An `initial_state/0` callback for state setup + - A unique `opts_key` for namespace isolation - Validation rules for configuration ### Signal Patterns @@ -88,7 +87,7 @@ defmodule Jido.Skill do category: "monitoring", tags: ["weather", "alerts"], vsn: "1.0.0", - schema_key: :weather, + opts_key: :weather, signals: [ input: ["weather.data.*", "weather.alert.**"], output: ["weather.alert.generated"] @@ -97,14 +96,6 @@ defmodule Jido.Skill do api_key: [type: :string, required: true] ] - def initial_state do - %{ - current_conditions: nil, - alert_history: [], - last_update: nil - } - end - def child_spec(config) do [ {WeatherAPI.Client, config.api_key} @@ -122,23 +113,23 @@ defmodule Jido.Skill do Skills implement these callbacks: - - `initial_state/0` - Returns the skill's initial state map - `child_spec/1` - Returns child process specifications - - `routes/0` - Returns signal routing rules + - `router/0` - Returns signal routing rules - `handle_signal/1` - Processes incoming signals - `process_result/2` - Post-processes signal handling results + - `mount/2` - Mounts the skill to an agent ## Behavior The Skill behavior enforces a consistent interface: ```elixir - @callback initial_state() :: map() @callback child_spec(config :: map()) :: Supervisor.child_spec() | [Supervisor.child_spec()] - @callback routes() :: [map()] + @callback router() :: [map()] @callback handle_signal(signal :: Signal.t()) :: {:ok, Signal.t()} | {:error, term()} @callback process_result(signal :: Signal.t(), result :: term()) :: {:ok, term()} | {:error, term()} + @callback mount(agent :: Jido.Agent.t(), opts :: keyword()) :: Jido.Agent.t() ``` ## Configuration @@ -150,14 +141,14 @@ defmodule Jido.Skill do - `category` - Broad classification - `tags` - List of searchable tags - `vsn` - Version string - - `schema_key` - State namespace key + - `opts_key` - State namespace key - `signals` - Input/output patterns - `config` - Configuration schema ## Best Practices 1. **State Isolation** - - Use meaningful schema_key names + - Use meaningful opts_key names - Keep state focused and minimal - Document state structure @@ -196,7 +187,7 @@ defmodule Jido.Skill do - `category`: Broad classification for organization - `tags`: List of searchable tags - `vsn`: Version string for compatibility - - `schema_key`: Atom key for state namespace + - `opts_key`: Atom key for state namespace - `signals`: Input/output signal patterns - `config`: Configuration schema """ @@ -206,7 +197,7 @@ defmodule Jido.Skill do field(:category, String.t()) field(:tags, [String.t()], default: []) field(:vsn, String.t()) - field(:schema_key, atom()) + field(:opts_key, atom()) field(:signals, map()) field(:config, map()) end @@ -239,11 +230,16 @@ defmodule Jido.Skill do required: false, doc: "The version of the Skill." ], - schema_key: [ + opts_key: [ type: :atom, required: true, doc: "Atom key for state namespace isolation" ], + opts_schema: [ + type: :keyword_list, + default: [], + doc: "Nimble Options schema for skill options" + ], signals: [ type: :map, required: true, @@ -252,11 +248,6 @@ defmodule Jido.Skill do input: [type: {:list, :string}, default: []], output: [type: {:list, :string}, default: []] ] - ], - config: [ - type: :map, - required: false, - doc: "Configuration schema" ] ) @@ -274,7 +265,7 @@ defmodule Jido.Skill do defmodule MySkill do use Jido.Skill, name: "my_skill", - schema_key: :my_skill, + opts_key: :my_skill, signals: [ input: ["my.event.*"], output: ["my.result.*"] @@ -313,13 +304,13 @@ defmodule Jido.Skill do def vsn, do: @validated_opts[:vsn] @doc false - def schema_key, do: @validated_opts[:schema_key] + def opts_key, do: @validated_opts[:opts_key] @doc false def signals, do: @validated_opts[:signals] @doc false - def config_schema, do: @validated_opts[:config] + def opts_schema, do: @validated_opts[:opts_schema] @doc false def to_json do @@ -329,9 +320,9 @@ defmodule Jido.Skill do category: @validated_opts[:category], tags: @validated_opts[:tags], vsn: @validated_opts[:vsn], - schema_key: @validated_opts[:schema_key], + opts_key: @validated_opts[:opts_key], signals: @validated_opts[:signals], - config_schema: @validated_opts[:config] + opts_schema: @validated_opts[:opts_schema] } end @@ -341,14 +332,11 @@ defmodule Jido.Skill do end # Default implementations - @doc false - def initial_state, do: %{} - @doc false def child_spec(_config), do: [] @doc false - def routes, do: [] + def router(_opts), do: [] @doc false def handle_signal(signal), do: {:ok, signal} @@ -356,11 +344,14 @@ defmodule Jido.Skill do @doc false def process_result(signal, result), do: {:ok, result} - defoverridable initial_state: 0, - child_spec: 1, - routes: 0, + @doc false + def mount(agent, _opts), do: {:ok, agent} + + defoverridable child_spec: 1, + router: 1, handle_signal: 1, - process_result: 2 + process_result: 2, + mount: 2 {:error, error} -> message = Error.format_nimble_config_error(error, "Skill", __MODULE__) @@ -374,12 +365,13 @@ defmodule Jido.Skill do end # Behaviour callbacks - @callback initial_state() :: map() @callback child_spec(config :: map()) :: Supervisor.child_spec() | [Supervisor.child_spec()] - @callback routes() :: [map()] + @callback router(skill_opts :: keyword()) :: [Route.t()] @callback handle_signal(signal :: Signal.t()) :: {:ok, Signal.t()} | {:error, term()} @callback process_result(signal :: Signal.t(), result :: term()) :: {:ok, term()} | {:error, any()} + @callback mount(agent :: Jido.Agent.t(), opts :: keyword()) :: + {:ok, Jido.Agent.t()} | {:error, Error.t()} @doc """ Skills must be defined at compile time, not runtime. @@ -410,14 +402,14 @@ defmodule Jido.Skill do ## Example - Skill.validate_config(WeatherSkill, %{ + Skill.validate_opts(WeatherSkill, %{ api_key: "abc123", interval: 1000 }) """ - @spec validate_config(module(), map()) :: {:ok, map()} | {:error, Error.t()} - def validate_config(skill_module, config) do - with {:ok, schema} <- get_config_schema(skill_module) do + @spec validate_opts(module(), map()) :: {:ok, map()} | {:error, Error.t()} + def validate_opts(skill_module, config) do + with {:ok, schema} <- get_opts_schema(skill_module) do NimbleOptions.validate(config, schema) end end @@ -436,14 +428,14 @@ defmodule Jido.Skill do Skill.get_config_schema(WeatherSkill) """ - @spec get_config_schema(module()) :: {:ok, map()} | {:error, Error.t()} - def get_config_schema(skill_module) do - case function_exported?(skill_module, :config_schema, 0) do + @spec get_opts_schema(module()) :: {:ok, map()} | {:error, Error.t()} + def get_opts_schema(skill_module) do + case function_exported?(skill_module, :opts_schema, 0) do true -> - {:ok, skill_module.config_schema()} + {:ok, skill_module.opts_schema()} false -> - {:error, Error.config_error("Skill has no config schema")} + {:error, Error.config_error("Skill has no opts schema")} end end diff --git a/lib/jido/skills/arithmetic.ex b/lib/jido/skills/arithmetic.ex index 69f49a4..879ebfc 100644 --- a/lib/jido/skills/arithmetic.ex +++ b/lib/jido/skills/arithmetic.ex @@ -11,7 +11,7 @@ defmodule Jido.Skills.Arithmetic do category: "math", tags: ["math", "arithmetic", "calculations"], vsn: "1.0.0", - schema_key: :arithmetic, + opts_key: :arithmetic, signals: %{ input: [ "arithmetic.add", @@ -26,14 +26,14 @@ defmodule Jido.Skills.Arithmetic do "arithmetic.error" ] }, - config: %{ + opts_schema: [ max_value: [ type: :integer, required: false, default: 1_000_000, doc: "Maximum allowed value for calculations" ] - } + ] defmodule Actions do @moduledoc false @@ -166,8 +166,8 @@ defmodule Jido.Skills.Arithmetic do * arithmetic.result: Result of arithmetic operation * arithmetic.error: Error from arithmetic operation """ - @spec routes() :: [map()] - def routes do + @spec router() :: [map()] + def router(_opts \\ []) do [ %{ path: "arithmetic.add", @@ -208,24 +208,6 @@ defmodule Jido.Skills.Arithmetic do ] end - @doc """ - Get the initial state for the arithmetic skill. - """ - @spec initial_state() :: map() - def initial_state do - %{ - last_result: nil, - operation_count: %{ - add: 0, - subtract: 0, - multiply: 0, - divide: 0, - square: 0, - eval: 0 - } - } - end - @doc """ Handle an arithmetic signal. """ diff --git a/lib/jido/util.ex b/lib/jido/util.ex index a9aad95..2a39a9d 100644 --- a/lib/jido/util.ex +++ b/lib/jido/util.ex @@ -111,7 +111,7 @@ defmodule Jido.Util do {:ok, [ValidAction]} """ @spec validate_actions(list(module()) | module()) :: - {:ok, list(module())} | {:error, String.t()} + {:ok, list(module()) | module()} | {:error, String.t()} def validate_actions(actions) when is_list(actions) do if Enum.all?(actions, &implements_action?/1) do {:ok, actions} @@ -121,7 +121,11 @@ defmodule Jido.Util do end def validate_actions(action) when is_atom(action) do - validate_actions([action]) + if implements_action?(action) do + {:ok, action} + else + {:error, "All actions must implement the Jido.Action behavior"} + end end defp implements_action?(module) when is_atom(module) do diff --git a/test/jido/agent/server_callback_test.exs b/test/jido/agent/server_callback_test.exs index 3b476f6..684a29c 100644 --- a/test/jido/agent/server_callback_test.exs +++ b/test/jido/agent/server_callback_test.exs @@ -22,7 +22,7 @@ defmodule Jido.Agent.Server.CallbackTest do skill = %TestSkill{ name: "test_skill", description: "Test skill for callback testing", - schema_key: :test_skill, + opts_key: :test_skill, signals: %{ input: ["test.skill.*"], output: ["test.skill.result"] diff --git a/test/jido/agent/server_skills_test.exs b/test/jido/agent/server_skills_test.exs index 806c9b5..1d85c43 100644 --- a/test/jido/agent/server_skills_test.exs +++ b/test/jido/agent/server_skills_test.exs @@ -4,52 +4,14 @@ defmodule Jido.Agent.Server.SkillsTest do alias Jido.Agent.Server.Skills alias Jido.Agent.Server.State, as: ServerState - alias JidoTest.TestSkills.MockSkill alias Jido.Signal.Router alias Jido.Instruction alias JidoTest.TestAgents.BasicAgent - alias JidoTest.TestSkills.{MockSkillWithRouter} - - # Mock skill module with router function - defmodule MockSkillWithRouter do - def router do - [ - %Router.Route{ - path: "test.path", - target: %Instruction{action: :test_handler}, - priority: 0 - } - ] - end - end - - # Mock skill module with invalid router - defmodule InvalidRouterSkill do - def router do - :not_a_list - end - end - - # Mock skill module - defmodule MockSkill do - def routes do - [ - %Router.Route{ - path: "test.path", - target: %Instruction{action: :test_handler}, - priority: 0 - } - ] - end - - def child_spec(_) do - %{ - id: __MODULE__, - start: {__MODULE__, :start_link, []}, - type: :worker - } - end - end + alias JidoTest.TestSkills.{ + MockSkill, + MockSkillWithSchema, + MockSkillWithMount + } describe "build/2" do setup do @@ -77,8 +39,8 @@ defmodule Jido.Agent.Server.SkillsTest do assert {:ok, updated_state, updated_opts} = Skills.build(state, opts) assert [MockSkill] == updated_state.skills - assert Keyword.get(updated_opts, :routes) == MockSkill.routes() - assert [MockSkill.child_spec([])] == Keyword.get(updated_opts, :child_specs) + assert Keyword.get(updated_opts, :routes) == MockSkill.router() + assert [MockSkill.child_spec()] == Keyword.get(updated_opts, :child_specs) end test "handles multiple skills", %{state: state} do @@ -87,10 +49,10 @@ defmodule Jido.Agent.Server.SkillsTest do assert {:ok, updated_state, updated_opts} = Skills.build(state, opts) assert [MockSkill, MockSkill] == updated_state.skills - expected_routes = MockSkill.routes() ++ MockSkill.routes() + expected_routes = MockSkill.router() ++ MockSkill.router() assert Keyword.get(updated_opts, :routes) == expected_routes - expected_child_specs = [MockSkill.child_spec([]), MockSkill.child_spec([])] + expected_child_specs = [MockSkill.child_spec(), MockSkill.child_spec()] assert Keyword.get(updated_opts, :child_specs) == expected_child_specs end @@ -130,11 +92,64 @@ defmodule Jido.Agent.Server.SkillsTest do assert [MockSkill] == updated_state.skills # Check routes are combined - assert existing_routes ++ MockSkill.routes() == Keyword.get(updated_opts, :routes) + assert existing_routes ++ MockSkill.router() == Keyword.get(updated_opts, :routes) # Check child_specs are combined - assert [MockSkill.child_spec([]), existing_child_spec] == + assert [MockSkill.child_spec(), existing_child_spec] == Keyword.get(updated_opts, :child_specs) end + + test "validates skill options and stores them in agent state", %{state: state} do + opts = [ + skills: [MockSkillWithSchema], + mock_skill_with_schema: [api_key: "test_key", timeout: 10000] + ] + + assert {:ok, updated_state, _updated_opts} = Skills.build(state, opts) + assert [MockSkillWithSchema] == updated_state.skills + + # Check that the validated options are stored in the agent state + stored_opts = updated_state.agent.state[:mock_skill_with_schema] + assert Keyword.get(stored_opts, :api_key) == "test_key" + assert Keyword.get(stored_opts, :timeout) == 10000 + end + + test "returns error when skill options validation fails", %{state: state} do + opts = [ + skills: [MockSkillWithSchema], + # Missing required api_key + mock_skill_with_schema: [timeout: 10000] + ] + + assert {:error, error_message} = Skills.build(state, opts) + assert String.contains?(error_message, "Failed to validate options for skill mock_skill_with_schema") + end + + test "uses default values for skill options when not provided", %{state: state} do + opts = [ + skills: [MockSkillWithSchema], + mock_skill_with_schema: [api_key: "test_key"] + ] + + assert {:ok, updated_state, _updated_opts} = Skills.build(state, opts) + + # Check that the default timeout value is used + stored_opts = updated_state.agent.state[:mock_skill_with_schema] + assert Keyword.get(stored_opts, :api_key) == "test_key" + assert Keyword.get(stored_opts, :timeout) == 5000 + end + + test "calls mount callback and transforms agent", %{state: state} do + opts = [skills: [MockSkillWithMount]] + + assert {:ok, updated_state, _updated_opts} = Skills.build(state, opts) + assert [MockSkillWithMount] == updated_state.skills + + # Check that the mount callback was called and transformed the agent + assert updated_state.agent.state[:mount_called] == true + + # Verify the action was registered + assert Enum.member?(updated_state.agent.actions, JidoTest.TestActions.BasicAction) + end end end diff --git a/test/jido/skills/skill_definition_test.exs b/test/jido/skills/skill_definition_test.exs index 67ce264..fc3e49f 100644 --- a/test/jido/skills/skill_definition_test.exs +++ b/test/jido/skills/skill_definition_test.exs @@ -14,7 +14,7 @@ defmodule Jido.SkillDefinitionTest do assert WeatherMonitorSkill.category() == "monitoring" assert WeatherMonitorSkill.tags() == ["weather", "alerts", "monitoring"] assert WeatherMonitorSkill.vsn() == "1.0.0" - assert WeatherMonitorSkill.schema_key() == :weather + assert WeatherMonitorSkill.opts_key() == :weather end test "skill metadata is accessible" do @@ -25,7 +25,7 @@ defmodule Jido.SkillDefinitionTest do assert metadata.category == "monitoring" assert metadata.tags == ["weather", "alerts", "monitoring"] assert metadata.vsn == "1.0.0" - assert metadata.schema_key == :weather + assert metadata.opts_key == :weather assert metadata.signals == %{ input: [ @@ -40,10 +40,10 @@ defmodule Jido.SkillDefinitionTest do ] } - assert metadata.config_schema == %{ + assert metadata.opts_schema == [ weather_api: [type: :map, required: true, doc: "Weather API configuration"], alerts: [type: :map, required: false, doc: "Alert configuration"] - } + ] end test "skill can be serialized to JSON" do @@ -54,7 +54,7 @@ defmodule Jido.SkillDefinitionTest do assert json.category == "monitoring" assert json.tags == ["weather", "alerts", "monitoring"] assert json.vsn == "1.0.0" - assert json.schema_key == :weather + assert json.opts_key == :weather end test "skill defines valid signal patterns" do @@ -73,17 +73,6 @@ defmodule Jido.SkillDefinitionTest do ] end - test "skill defines initial state" do - initial_state = WeatherMonitorSkill.initial_state() - - assert initial_state == %{ - current_conditions: nil, - alert_history: [], - last_report: nil, - locations: ["NYC", "LA", "CHI"] - } - end - test "skill defines child specs" do config = %{ weather_api: %{api_key: "test"}, diff --git a/test/support/test_skills.ex b/test/support/test_skills.ex index 8e88212..347916d 100644 --- a/test/support/test_skills.ex +++ b/test/support/test_skills.ex @@ -6,13 +6,13 @@ defmodule JidoTest.TestSkills do use Jido.Skill, name: "test_skill", description: "Test skill for callback testing", - schema_key: :test_skill, + opts_key: :test_skill, signals: %{ input: ["test.skill.*"], output: ["test.skill.result"] } - defstruct [:name, :description, :schema_key, :signals] + defstruct [:name, :description, :opts_key, :signals] def handle_signal(signal) do {:ok, %{signal | data: Map.put(signal.data, :skill_handled, true)}} @@ -31,7 +31,7 @@ defmodule JidoTest.TestSkills do category: "monitoring", tags: ["weather", "alerts", "monitoring"], vsn: "1.0.0", - schema_key: :weather, + opts_key: :weather, signals: %{ # Input signals this skill handles input: [ @@ -46,7 +46,7 @@ defmodule JidoTest.TestSkills do "weather_monitor.conditions.changed" ] }, - config: %{ + opts_schema: [ weather_api: [ type: :map, required: true, @@ -57,9 +57,9 @@ defmodule JidoTest.TestSkills do required: false, doc: "Alert configuration" ] - } + ] - defstruct [:name, :description, :category, :tags, :vsn, :schema_key, :signals, :config] + defstruct [:name, :description, :category, :tags, :vsn, :opts_key, :signals, :config] # Actions that this skill provides to the agent defmodule Actions do @@ -236,16 +236,6 @@ defmodule JidoTest.TestSkills do ] end - # Optional: Initial state for the skill's keyspace - def initial_state do - %{ - current_conditions: nil, - alert_history: [], - last_report: nil, - locations: ["NYC", "LA", "CHI"] - } - end - def handle_result( {:ok, %{weather_data: data}}, "weather_monitor.data.received" @@ -292,15 +282,33 @@ defmodule JidoTest.TestSkills do end end + # Mock skill for testing defmodule MockSkill do - @moduledoc false - def routes do + @moduledoc """ + A basic mock skill for testing. + """ + use Jido.Skill, + name: "mock_skill", + description: "Basic mock skill for testing", + opts_key: :mock_skill, + signals: %{ + input: ["test.path.*"], + output: ["test.result.*"] + } + + @impl true + def router(_opts \\ []) do [ - {:test_route, :test_handler} + %Jido.Signal.Router.Route{ + path: "test.path", + target: %Jido.Instruction{action: :test_handler}, + priority: 0 + } ] end - def child_spec(_) do + @impl true + def child_spec(_opts \\ []) do %{ id: __MODULE__, start: {__MODULE__, :start_link, []}, @@ -309,18 +317,151 @@ defmodule JidoTest.TestSkills do end end - defmodule InvalidSkill do - @moduledoc false - def routes do + # Mock skill with router function + defmodule MockSkillWithRouter do + @moduledoc """ + A mock skill with a router function. + """ + use Jido.Skill, + name: "mock_skill_with_router", + description: "Mock skill with router function", + opts_key: :mock_skill_with_router, + signals: %{ + input: ["test.path.*"], + output: ["test.result.*"] + } + + @impl true + def router(_opts \\ []) do + [ + %Jido.Signal.Router.Route{ + path: "test.path", + target: %Jido.Instruction{action: :test_handler}, + priority: 0 + } + ] + end + + @impl true + def child_spec(_opts), do: [] + end + + # Mock skill with invalid router + defmodule InvalidRouterSkill do + @moduledoc """ + A mock skill with an invalid router. + """ + use Jido.Skill, + name: "invalid_router_skill", + description: "Mock skill with invalid router", + opts_key: :invalid_router_skill, + signals: %{ + input: ["test.path.*"], + output: ["test.result.*"] + } + + @impl true + def router(_opts \\ []) do :not_a_list end - def child_spec(_) do + @impl true + def child_spec(_opts), do: [] + end + + # Mock skill with validation schema + defmodule MockSkillWithSchema do + @moduledoc """ + A mock skill with a validation schema. + """ + use Jido.Skill, + name: "mock_skill_with_schema", + description: "Mock skill with validation schema", + opts_key: :mock_skill_with_schema, + signals: %{ + input: ["test.path.*"], + output: ["test.result.*"] + }, + opts_schema: [ + api_key: [ + type: :string, + required: true, + doc: "API key for the service" + ], + timeout: [ + type: :integer, + default: 5000, + doc: "Timeout in milliseconds" + ] + ] + + @impl true + def router(_opts \\ []) do + [ + %Jido.Signal.Router.Route{ + path: "test.path", + target: %Jido.Instruction{action: :test_handler}, + priority: 0 + } + ] + end + + @impl true + def child_spec(_opts \\ []) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, []}, + type: :worker + } + end + end + + # Mock skill with custom mount implementation + defmodule MockSkillWithMount do + @moduledoc """ + A mock skill with a custom mount implementation that registers a custom action module. + """ + use Jido.Skill, + name: "mock_skill_with_mount", + description: "Mock skill with custom mount implementation", + opts_key: :mock_skill_with_mount, + signals: %{ + input: ["test.path.*"], + output: ["test.result.*"] + }, + opts_schema: [] + + @impl true + def router(_opts \\ []) do + [ + %Jido.Signal.Router.Route{ + path: "test.path", + target: %Jido.Instruction{action: :test_handler}, + priority: 0 + } + ] + end + + @impl true + def child_spec(_opts \\ []) do %{ id: __MODULE__, start: {__MODULE__, :start_link, []}, type: :worker } end + + @impl true + def mount(agent, _opts) do + # Register an existing action from JidoTest.TestActions + {:ok, updated_agent} = Jido.Agent.register_action(agent, JidoTest.TestActions.BasicAction) + + # Update the agent state to verify the mount was called + updated_agent = Map.update!(updated_agent, :state, fn state -> + Map.put(state, :mount_called, true) + end) + + {:ok, updated_agent} + end end end