Skip to content

Commit

Permalink
Refactor Skill module with enhanced configuration and lifecycle manag…
Browse files Browse the repository at this point in the history
…ement

- Replace `schema_key` with `opts_key` for more descriptive state isolation
- Remove `initial_state/0` callback and simplify skill configuration
- Add `mount/2` callback to allow skills to transform agent state
- Enhance skill options validation with `validate_opts/2`
- Update routing mechanism from `routes/0` to `router/1`
- Improve skill metadata and configuration handling
- Update test infrastructure to support new skill lifecycle
  • Loading branch information
mikehostetler committed Feb 27, 2025
1 parent 8089c74 commit 0e91d96
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 217 deletions.
27 changes: 2 additions & 25 deletions lib/jido/agent/server_callback.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down Expand Up @@ -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
129 changes: 101 additions & 28 deletions lib/jido/agent/server_skills.ex
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 0e91d96

Please sign in to comment.