Packages
fnord
0.7.10
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/ai/tools.ex
defmodule AI.Tools do
@moduledoc """
This module defines the behaviour for tool calls. Defining a new tool
requires implementing the `spec/0` and `call/2` functions.
The `spec/0` function should return a map that describes the tool's
capabilities and arguments, using a map to represent the OpenAPI spec.
The `call/2` function generates the tool call response. It accepts the
requesting `AI.Completion`'s struct and a map derived from the parsed JSON
provided by the agent, containing the tool call arguments. Note that, because
the arguments are parsed from JSON, the keys will all be strings. Whether
those are converted to symbols is between the tool implementation and the
code it calls. What happens behind closed APIs is none of MY business.
## Skeleton Implementation
```elixir
defmodule AI.Tools.MyNewTool do
@behaviour AI.Tools
@impl AI.Tools
def ui_note_on_request(_args) do
{"Doing something", "This tool is doing something."}
end
@impl AI.Tools
def ui_note_on_result(_args, _result) do
{"Did something", "This tool did something."}
end
@impl AI.Tools
def read_args(args) do
{:ok, args}
end
@impl AI.Tools
def spec() do
%{
type: "function",
function: %{
name: "something_tool",
description: "This tool does something.",
strict: true,
parameters: %{
additionalProperties: false,
type: "object",
required: ["thing"],
properties: %{
thing: %{
type: "string",
description: "The thing to do."
}
}
}
}
}
end
@impl AI.Tools
def call(_completion, args) do
{:ok, "IMPLEMENT ME"}
end
end
```
"""
@type args_error ::
{:error, :missing_argument, String.t()}
| {:error, :invalid_argument, String.t()}
@doc """
Returns the OpenAPI spec for the tool as an elixir map.
"""
@callback spec() :: map
@doc """
Calls the tool with the provided arguments and returns the response as an :ok
tuple.
"""
@callback call(completion :: AI.Completion.t(), args :: map) ::
:ok
| {:ok, any}
| {:error, any}
| :error
@doc """
Reads the arguments and returns a map of the arguments if they are valid.
This is used to validate args before the tool is called. The result is what
is passed to `call/2`, `ui_note_on_request/1`, and `ui_note_on_result/2`.
"""
@callback read_args(args :: map) :: {:ok, map} | args_error
@doc """
Return either a short string or a string tuple of label + detail to be
displayed when the tool is called.
"""
@callback ui_note_on_request(args :: map) ::
{String.t(), String.t()}
| String.t()
| nil
@doc """
Return either a short string or a string tuple of label + detail to be
displayed when the tool call is successful.
"""
@callback ui_note_on_result(args :: map, result :: any) ::
{String.t(), String.t()}
| String.t()
| nil
# ----------------------------------------------------------------------------
# Tool Registry
# ----------------------------------------------------------------------------
@default_tools %{
"file_contents_tool" => AI.Tools.File.Contents,
"file_info_tool" => AI.Tools.File.Info,
"file_list_tool" => AI.Tools.File.List,
"file_outline_tool" => AI.Tools.File.Outline,
"file_search_tool" => AI.Tools.File.Search,
"file_spelunker_tool" => AI.Tools.File.Spelunker,
"ripgrep_search" => AI.Tools.Ripgrep
}
@git_tools %{
"git_diff_branch_tool" => AI.Tools.Git.DiffBranch,
"git_grep_tool" => AI.Tools.Git.Grep,
"git_list_branches_tool" => AI.Tools.Git.ListBranches,
"git_log_tool" => AI.Tools.Git.Log,
"git_pickaxe_tool" => AI.Tools.Git.Pickaxe,
"git_show_tool" => AI.Tools.Git.Show
}
@tools %{}
|> Map.merge(@default_tools)
|> Map.merge(@git_tools)
# ----------------------------------------------------------------------------
# API Functions
# ----------------------------------------------------------------------------
def tools, do: @tools
def frobs(project \\ nil) do
Frobs.list(project)
|> Enum.map(&{&1.name, Frobs.create_tool_module(&1)})
|> Map.new()
end
def all_tools(project \\ nil) do
@tools
|> Map.merge(frobs(project))
end
def tool_module(tool, tools \\ @tools) do
case Map.get(tools, tool) do
nil -> {:error, :unknown_tool, tool}
module -> {:ok, module}
end
end
def tool_spec!(tool, tools \\ @tools) do
with {:ok, module} <- tool_module(tool, tools) do
module.spec()
else
{:error, :unknown_tool, tool} ->
raise ArgumentError, "Unknown tool: #{tool}"
end
end
def tool_spec(tool, tools \\ @tools) do
with {:ok, module} <- tool_module(tool, tools) do
{:ok, module.spec()}
end
end
def perform_tool_call(state, tool, args, tools \\ @tools) do
with {:ok, module} <- tool_module(tool, tools),
{:ok, args} <- module.read_args(args),
:ok <- validate_required_args(tool, args, tools) do
module.call(state, args)
end
end
def on_tool_request(tool, args, tools \\ @tools) do
with {:ok, module} <- tool_module(tool, tools),
{:ok, args} <- module.read_args(args),
:ok <- validate_required_args(tool, args, tools) do
try do
module.ui_note_on_request(args)
rescue
e ->
UI.error(
"Error logging tool call request for <#{tool}> (args: #{inspect(args)})",
inspect(e)
)
nil
end
else
{:error, :missing_argument, _key} ->
nil
error ->
UI.error(
"Error logging tool call request for <#{tool}> (args: #{inspect(args)})",
inspect(error)
)
nil
end
end
def on_tool_result(tool, args, result, tools \\ @tools) do
with {:ok, module} <- tool_module(tool, tools),
{:ok, args} <- module.read_args(args) do
try do
module.ui_note_on_result(args, result)
rescue
e in ArgumentError ->
{
"Error logging tool call result for <#{tool}> (args: #{inspect(args)})",
inspect(e)
}
end
end
end
def validate_required_args(tool, args, tools \\ @tools) do
with {:ok, module} <- tool_module(tool, tools) do
module.spec()
|> Map.get(:function)
|> Map.get(:parameters)
|> Map.get(:required)
|> check_required_args(args)
end
end
def required_arg_error(key) do
{:error, :missing_argument, key}
end
def with_args(tool, args, fun, tools \\ @tools) do
with {:ok, module} <- tool_module(tool, tools),
{:ok, args} <- module.read_args(args) do
fun.(args)
end
end
# ----------------------------------------------------------------------------
# Common Utility Functions
# ----------------------------------------------------------------------------
def get_project() do
project = Store.get_project()
if Store.Project.exists_in_store?(project) do
{:ok, project}
else
{:error, :project_not_found}
end
end
def get_entry(file) do
with {:ok, project} <- get_project() do
get_entry(project, file)
end
end
def get_entry(project, file) do
Store.Project.find_entry(project, file)
end
def get_file_contents(file) do
Store.get_project()
|> Store.Project.find_file(file)
|> case do
{:ok, path} -> File.read(path)
{:error, :not_found} -> {:error, :enoent}
end
end
# ----------------------------------------------------------------------------
# Private Functions
# ----------------------------------------------------------------------------
defp check_required_args([], _args) do
:ok
end
defp check_required_args([key | keys], args) do
with :ok <- get_arg(args, key) do
check_required_args(keys, args)
else
_ -> required_arg_error(key)
end
end
defp get_arg(args, key) do
args
|> Map.fetch(key)
|> case do
:error -> required_arg_error(key)
{:ok, ""} -> required_arg_error(key)
{:ok, nil} -> required_arg_error(key)
_ -> :ok
end
end
end