Current section

Files

Jump to
nous lib nous tool behaviour.ex
Raw

lib/nous/tool/behaviour.ex

defmodule Nous.Tool.Behaviour do
@moduledoc """
Behaviour for tool implementations.
Allows tools to be defined as modules for better testability and organization.
Module-based tools can inject dependencies via the context, making them
easy to test with mocks.
## Example
defmodule MyApp.Tools.Search do
@behaviour Nous.Tool.Behaviour
@impl true
def metadata do
%{
name: "search",
description: "Search the web for information",
parameters: %{
"type" => "object",
"properties" => %{
"query" => %{
"type" => "string",
"description" => "The search query"
}
},
"required" => ["query"]
}
}
end
@impl true
def execute(ctx, %{"query" => query}) do
# Inject http_client via deps for testing
http_client = ctx.deps[:http_client] || Nous.HTTP
api_key = ctx.deps[:search_api_key]
case http_client.get("https://api.search.com", query: query, key: api_key) do
{:ok, results} -> {:ok, format_results(results)}
{:error, _} = err -> err
end
end
defp format_results(results), do: Enum.map(results, & &1["title"])
end
## Testing
defmodule MyApp.Tools.SearchTest do
use ExUnit.Case
test "search returns formatted results" do
mock_http = %{
get: fn _url, _opts ->
{:ok, [%{"title" => "Result 1"}, %{"title" => "Result 2"}]}
end
}
ctx = Nous.RunContext.new(%{http_client: mock_http, search_api_key: "test"})
assert {:ok, ["Result 1", "Result 2"]} =
MyApp.Tools.Search.execute(ctx, %{"query" => "elixir"})
end
end
## Usage with Agent
agent = Nous.Agent.new("openai:gpt-4",
tools: [Nous.Tool.from_module(MyApp.Tools.Search)]
)
"""
@doc """
Execute the tool with context and arguments.
The context provides access to dependencies and execution metadata.
Arguments are a map of the parameters passed by the LLM.
## Return Values
- `{:ok, result}` - Success with the result to return to the LLM
- `{:ok, result, context_update}` - Success with context updates (see `Nous.Tool.ContextUpdate`)
- `{:error, reason}` - Failure with error reason
"""
@callback execute(ctx :: Nous.RunContext.t(), args :: map()) ::
{:ok, any()}
| {:ok, any(), Nous.Tool.ContextUpdate.t()}
| {:error, term()}
@doc """
Return tool metadata (name, description, parameters).
This callback is optional. If not implemented, the tool name will be
derived from the module name.
## Return Value
%{
name: "tool_name",
description: "What the tool does",
parameters: %{
"type" => "object",
"properties" => %{...},
"required" => [...]
}
}
"""
@callback metadata() :: %{
name: String.t(),
description: String.t(),
parameters: map()
}
@optional_callbacks [metadata: 0]
@doc """
Check if a module implements the Tool.Behaviour.
"""
@spec implements?(module()) :: boolean()
def implements?(module) when is_atom(module) do
function_exported?(module, :execute, 2)
end
@doc """
Get metadata from a module, using defaults if metadata/0 is not implemented.
"""
@spec get_metadata(module()) :: map()
def get_metadata(module) when is_atom(module) do
if function_exported?(module, :metadata, 0) do
module.metadata()
else
%{
name: module_to_name(module),
description: "",
parameters: default_schema()
}
end
end
@doc """
Convert a module name to a tool name.
## Examples
iex> Nous.Tool.Behaviour.module_to_name(MyApp.Tools.SearchDatabase)
"search_database"
"""
@spec module_to_name(module()) :: String.t()
def module_to_name(module) when is_atom(module) do
module
|> Module.split()
|> List.last()
|> Macro.underscore()
end
defp default_schema do
%{
"type" => "object",
"properties" => %{},
"required" => []
}
end
end