Packages
nous
0.15.2
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/plugins/skills.ex
defmodule Nous.Plugins.Skills do
@moduledoc """
Plugin that integrates the Skills system into the agent lifecycle.
This plugin bridges `Nous.Skill` definitions into the existing plugin
pipeline, handling skill discovery, activation, and injection of
instructions and tools.
## Automatic Inclusion
When `skills: [...]` is provided to `Nous.Agent.new/2`, this plugin is
automatically added to the plugins list.
## Lifecycle
1. **init** — resolves skill specs, builds registry, auto-activates `:auto` skills
2. **system_prompt** — injects instructions from all active skills
3. **tools** — provides tools from all active skills
4. **before_request** — matches user input against skills for dynamic activation
## Example
agent = Agent.new("openai:gpt-4",
skills: [
MyApp.Skills.CodeReview,
"priv/skills/",
{:group, :testing}
]
)
"""
@behaviour Nous.Plugin
alias Nous.Skill.Registry
require Logger
@impl true
def init(agent, ctx) do
if agent.skills == [] do
ctx
else
# Resolve all skill specs into a registry
registry = Registry.resolve(agent.skills)
Logger.debug("Skills plugin initialized with #{length(Registry.list(registry))} skill(s)")
# Auto-activate skills with activation: :auto
registry =
registry.skills
|> Map.values()
|> Enum.filter(&(&1.activation == :auto))
|> Enum.reduce(registry, fn skill, reg ->
{_instructions, _tools, reg} = Registry.activate(reg, skill.name, agent, ctx)
reg
end)
%{ctx | deps: Map.put(ctx.deps, :skill_registry, registry)}
end
end
@impl true
def system_prompt(agent, ctx) do
registry = get_registry(ctx)
if registry do
active = Registry.active_skills(registry)
if Enum.empty?(active) do
nil
else
active
|> Enum.map(fn skill ->
{instructions, _tools} = load_instructions(skill, agent, ctx)
if instructions && instructions != "" do
"## Skill: #{skill.name}\n\n#{instructions}"
end
end)
|> Enum.reject(&is_nil/1)
|> case do
[] -> nil
parts -> Enum.join(parts, "\n\n---\n\n")
end
end
end
end
@impl true
def tools(agent, ctx) do
registry = get_registry(ctx)
if registry do
Registry.active_skills(registry)
|> Enum.flat_map(fn skill ->
{_instructions, tools} = load_instructions(skill, agent, ctx)
tools
end)
else
[]
end
end
@impl true
def before_request(agent, ctx, tools) do
registry = get_registry(ctx)
if registry do
# Get latest user message for matching
user_input = get_latest_user_input(ctx)
if user_input do
# Find matching skills that aren't already active
matched = Registry.match(registry, user_input)
registry =
Enum.reduce(matched, registry, fn skill, reg ->
unless Registry.active?(reg, skill.name) do
Logger.debug("Auto-activating skill: #{skill.name}")
{_instructions, _tools, reg} = Registry.activate(reg, skill.name, agent, ctx)
reg
else
reg
end
end)
# Collect tools from newly activated skills
new_tools =
matched
|> Enum.flat_map(fn skill ->
{_instructions, skill_tools} = load_instructions(skill, agent, ctx)
skill_tools
end)
ctx = %{ctx | deps: Map.put(ctx.deps, :skill_registry, registry)}
{ctx, tools ++ new_tools}
else
{ctx, tools}
end
else
{ctx, tools}
end
end
# Private helpers
defp get_registry(ctx) do
Map.get(ctx.deps, :skill_registry)
end
defp load_instructions(%Nous.Skill{source: :module, source_ref: module}, agent, ctx) do
instructions = module.instructions(agent, ctx)
tools =
if function_exported?(module, :tools, 2) do
module.tools(agent, ctx)
else
[]
end
{instructions, tools}
end
defp load_instructions(%Nous.Skill{instructions: instructions}, _agent, _ctx) do
{instructions, []}
end
defp get_latest_user_input(ctx) do
ctx.messages
|> Enum.reverse()
|> Enum.find_value(fn msg ->
if msg.role == :user do
case msg.content do
text when is_binary(text) ->
text
parts when is_list(parts) ->
Enum.find_value(parts, fn
%{type: :text, text: text} -> text
_ -> nil
end)
_ ->
nil
end
end
end)
end
end