Packages
fnord
0.8.11
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/completion.ex
defmodule AI.Completion do
@moduledoc """
This module sends a request to the model and handles the response. It is able
to handle tool calls and responses.
## Input options
- `toolbox` - a map of tool names to modules implementing `AI.Tools`; the specs list
is derived automatically via `AI.Tools.toolbox_to_specs/1`.
## Output options
Output is controlled by the following mechanisms.
1. `log_msgs` - log messages from the user and assistant as `info`
2. `log_tool_calls` - log tool calls as `info` and tool call results as `debug`
`LOGGER_LEVEL` must be set to `debug` to see the output of tool call results.
"""
defstruct [
:opts,
:model,
:toolbox,
:specs,
:log_msgs,
:log_tool_calls,
:archive_notes,
:replay_conversation,
:usage,
:messages,
:tool_call_requests,
:response
]
@type t :: %__MODULE__{
opts: Keyword.t(),
model: String.t(),
toolbox: AI.Tools.toolbox() | nil,
specs: list(AI.Tools.tool_spec()) | nil,
log_msgs: boolean(),
log_tool_calls: boolean(),
archive_notes: boolean(),
replay_conversation: boolean(),
usage: integer(),
messages: list(),
tool_call_requests: list(),
response: String.t() | nil
}
@type response :: {:ok, t} | {:error, t}
@spec get(Keyword.t()) :: response
def get(opts) do
with {:ok, state} <- new(opts) do
state
|> AI.Completion.Output.replay_conversation()
|> send_request()
end
end
def new(opts) do
with {:ok, model} <- Keyword.fetch(opts, :model),
{:ok, messages} <- Keyword.fetch(opts, :messages) do
toolbox =
opts
|> Keyword.get(:toolbox, nil)
|> AI.Tools.build_toolbox()
specs =
toolbox
|> Map.values()
|> Enum.map(& &1.spec())
log_msgs = Keyword.get(opts, :log_msgs, false)
replay = Keyword.get(opts, :replay_conversation, true)
quiet? = Application.get_env(:fnord, :quiet)
log_tool_calls = Keyword.get(opts, :log_tool_calls, !quiet?)
archive? = Keyword.get(opts, :archive_notes, false)
state = %__MODULE__{
opts: Enum.into(opts, %{}),
model: model,
toolbox: toolbox,
specs: specs,
log_msgs: log_msgs,
log_tool_calls: log_tool_calls,
archive_notes: archive?,
replay_conversation: replay,
usage: 0,
messages: messages,
tool_call_requests: [],
response: nil
}
{:ok, state}
end
end
def new_from_conversation(conversation, opts) do
if Store.Project.Conversation.exists?(conversation) do
{:ok, _ts, msgs} = Store.Project.Conversation.read(conversation)
new(Keyword.put(opts, :messages, msgs))
else
{:error, :conversation_not_found}
end
end
def tools_used(%{messages: messages}) do
messages
|> Enum.reduce(%{}, fn
%{tool_calls: tool_calls}, acc ->
tool_calls
|> Enum.reduce(acc, fn
%{function: %{name: func}}, acc ->
Map.update(acc, func, 1, &(&1 + 1))
end)
_, acc ->
acc
end)
end
# -----------------------------------------------------------------------------
# Completion handling
# -----------------------------------------------------------------------------
defp send_request(state) do
AI.CompletionAPI.get(
state.model,
state.messages,
state.specs
)
|> handle_response(state)
end
defp handle_response({:ok, :msg, response, usage}, state) do
{:ok,
%{
state
| messages: state.messages ++ [AI.Util.assistant_msg(response)],
response: response,
usage: usage
}}
end
defp handle_response({:ok, :tool, tool_calls}, state) do
%{state | tool_call_requests: tool_calls}
|> handle_tool_calls()
|> send_request()
end
defp handle_response({:error, %{http_status: http_status, code: code, message: msg}}, state) do
error_msg = """
I encountered an error while processing your request.
- HTTP Status: #{http_status}
- Error code: #{code}
- Message: #{msg}
"""
{:error, %{state | response: error_msg}}
end
defp handle_response({:error, %{http_status: http_status, message: msg}}, state) do
error_msg = """
I encountered an error while processing your request.
- HTTP Status: #{http_status}
- Message: #{msg}
"""
{:error, %{state | response: error_msg}}
end
defp handle_response({:error, reason}, state) do
reason =
if is_binary(reason) do
reason
else
inspect(reason, pretty: true)
end
error_msg = """
I encountered an error while processing your request.
The error message was:
#{reason}
"""
{:error, %{state | response: error_msg}}
end
# -----------------------------------------------------------------------------
# Tool calls
# -----------------------------------------------------------------------------
defp handle_tool_calls(%{tool_call_requests: tool_calls} = state) do
{confirm_reqs, non_confirm_reqs} =
Enum.split_with(tool_calls, &is_confirm_tool_request?/1)
# First handle non-confirmation tool calls concurrently
messages_non_confirm =
non_confirm_reqs
|> Util.async_stream(&handle_tool_call(state, &1))
|> Enum.reduce(state.messages, fn
{:ok, {:ok, msgs}}, acc -> acc ++ msgs
_, acc -> acc
end)
# Now handle all confirm_tool requests serially and append
messages_final =
Enum.reduce(confirm_reqs, messages_non_confirm, fn req, acc ->
{:ok, msgs} = handle_tool_call(state, req)
acc ++ msgs
end)
%{state | tool_call_requests: [], messages: messages_final}
end
def handle_tool_call(state, %{id: id, function: %{name: func, arguments: args_json}}) do
request = AI.Util.assistant_tool_msg(id, func, args_json)
with {:ok, output} <- perform_tool_call(state, func, args_json) do
if state.archive_notes do
NotesServer.ingest_research(func, args_json, output)
end
response = AI.Util.tool_msg(id, func, output)
{:ok, [request, response]}
else
{:error, reason} ->
oopsie(state, func, args_json, reason)
response = AI.Util.tool_msg(id, func, reason)
{:ok, [request, response]}
{:error, :unknown_tool, tool} ->
oopsie(state, func, args_json, "Invalid tool #{tool}")
error = """
Your attempt to call #{func} failed because the tool '#{tool}' was not found.
Your tool call request supplied the following arguments: #{args_json}.
Please consult the specifications for your available tools and use only the tools that are listed.
"""
response = AI.Util.tool_msg(id, func, error)
{:ok, [request, response]}
{:error, :missing_argument, key} ->
oopsie(state, func, args_json, "Missing required argument #{key}")
spec =
with {:ok, spec} <- AI.Tools.tool_spec(func, state.toolbox),
{:ok, json} <- Jason.encode(spec) do
json
else
error -> "Error retrieving specification: #{inspect(error)}"
end
error = """
Your attempt to call #{func} failed because it was missing a required argument, '#{key}'.
Your tool call request supplied the following arguments: #{args_json}.
The parameter `#{key}` must be included and cannot be `null` or an empty string.
The correct specification for the tool call is: #{spec}
"""
response = AI.Util.tool_msg(id, func, error)
{:ok, [request, response]}
{:error, :invalid_argument, key} ->
oopsie(state, func, args_json, "Invalid argument #{key}")
spec =
with {:ok, spec} <- AI.Tools.tool_spec(func, state.toolbox),
{:ok, json} <- Jason.encode(spec) do
json
else
error -> "Error retrieving specification: #{inspect(error)}"
end
error = """
Your attempt to call #{func} failed because it contained an invalid argument or value for '#{key}'.
Your tool call request supplied the following arguments: #{args_json}.
The parameter `#{key}` must be a valid value as specified in the tool's specification.
The correct specification for the tool call is: #{spec}
"""
response = AI.Util.tool_msg(id, func, error)
{:ok, [request, response]}
{:error, exit_code, msg} when is_integer(exit_code) ->
oopsie(state, func, args_json, "External process exited with code #{exit_code}: #{msg}")
error = """
Your attempt to call #{func} failed because the external process exited with an error.
Exit code: #{exit_code}
Error message: #{msg}
"""
response = AI.Util.tool_msg(id, func, error)
{:ok, [request, response]}
end
end
@spec perform_tool_call(t, binary, binary) :: AI.Tools.tool_result()
defp perform_tool_call(state, func, args_json) when is_binary(args_json) do
with {:ok, args} <- Jason.decode(args_json) do
AI.Tools.with_args(
func,
args,
fn args ->
AI.Completion.Output.on_event(state, :tool_call, {func, args})
result = AI.Tools.perform_tool_call(func, args, state.toolbox)
AI.Completion.Output.on_event(state, :tool_call_result, {func, args, result})
result
end,
state.toolbox
)
end
end
@spec oopsie(t, binary, binary, any) :: any
defp oopsie(state, tool, args_json, reason) do
AI.Completion.Output.on_event(
state,
:tool_call_error,
{tool, args_json, {:error, reason}}
)
end
# Returns true if this tool call request is a 'confirm_tool' function call.
defp is_confirm_tool_request?(%{function: %{name: "confirm_tool"}}), do: true
# Returns true if this tool call request is a 'confirm_tool' function call.
defp is_confirm_tool_request?(_), do: false
end