Packages
fnord
0.5.6
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.
"""
defstruct [
:ai,
:opts,
:max_tokens,
:model,
:use_planner,
:tools,
:log_msgs,
:log_tool_calls,
:log_tool_call_results,
:replay_conversation,
:messages,
:tool_call_requests,
:response
]
@type t :: %__MODULE__{
ai: AI.t(),
opts: Keyword.t(),
max_tokens: non_neg_integer(),
model: String.t(),
use_planner: boolean(),
tools: list(),
log_msgs: boolean(),
log_tool_calls: boolean(),
log_tool_call_results: boolean(),
replay_conversation: boolean(),
messages: list(),
tool_call_requests: list(),
response: String.t() | nil
}
@type success :: {:ok, t}
@type error :: {:error, String.t()}
@type response :: success | error
@spec get(AI.t(), Keyword.t()) :: response
def get(ai, opts) do
with {:ok, max_tokens} <- Keyword.fetch(opts, :max_tokens),
{:ok, model} <- Keyword.fetch(opts, :model),
{:ok, messages} <- Keyword.fetch(opts, :messages) do
tools = Keyword.get(opts, :tools, nil)
use_planner = Keyword.get(opts, :use_planner, false)
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?)
log_tool_call_results = Keyword.get(opts, :log_tool_call_results, !quiet?)
state = %__MODULE__{
ai: ai,
opts: Enum.into(opts, %{}),
max_tokens: max_tokens,
model: model,
use_planner: use_planner,
tools: tools,
log_msgs: log_msgs,
log_tool_calls: log_tool_calls,
log_tool_call_results: log_tool_call_results,
replay_conversation: replay,
messages: messages,
tool_call_requests: [],
response: nil
}
state
|> replay_conversation()
|> maybe_start_planner()
|> send_request()
|> maybe_finish_planner()
|> then(&{:ok, &1})
end
end
def context_window_usage(%{model: model, messages: msgs, max_tokens: max_tokens}) do
tokens = msgs |> inspect() |> AI.Tokenizer.encode(model) |> length()
pct = tokens / max_tokens * 100.0
pct_str = Number.Percentage.number_to_percentage(pct, precision: 2)
tokens_str = Number.Delimit.number_to_delimited(tokens, precision: 0)
max_tokens_str = Number.Delimit.number_to_delimited(max_tokens, precision: 0)
{"Context window usage", "#{pct_str} | #{tokens_str} / #{max_tokens_str}"}
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
state
|> maybe_use_planner()
|> get_completion()
|> handle_response()
end
def get_completion(state) do
response = AI.get_completion(state.ai, state.model, state.messages, state.tools)
{response, state}
end
defp handle_response({{:ok, :msg, response}, state}) do
%{
state
| messages: state.messages ++ [AI.Util.assistant_msg(response)],
response: response
}
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}
"""
%{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}
"""
%{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}
"""
%{state | response: error_msg}
end
# -----------------------------------------------------------------------------
# Planner
# -----------------------------------------------------------------------------
defp maybe_start_planner(%{use_planner: false} = state), do: state
defp maybe_start_planner(%{ai: ai, use_planner: true, messages: msgs, tools: tools} = state) do
log_tool_call(state, "Building a research plan")
case AI.Agent.Planner.get_response(ai, %{msgs: msgs, tools: tools, stage: :initial}) do
{:ok, response} ->
log_tool_call_result(state, "Research plan", response)
planner_msg = AI.Util.user_msg(response)
%__MODULE__{state | messages: state.messages ++ [planner_msg]}
{:error, reason} ->
log_tool_call_error(state, "planner", reason)
state
end
end
defp maybe_use_planner(%{use_planner: false} = state), do: state
defp maybe_use_planner(%{ai: ai, use_planner: true, messages: msgs, tools: tools} = state) do
log_tool_call(state, "Evaluating research and planning next steps")
case AI.Agent.Planner.get_response(ai, %{msgs: msgs, tools: tools, stage: :checkin}) do
{:ok, response} ->
log_tool_call_result(state, "Refining research plan", response)
planner_msg = AI.Util.user_msg(response)
%__MODULE__{state | messages: state.messages ++ [planner_msg]}
{:error, reason} ->
log_tool_call_error(state, "planner", reason)
state
end
end
defp maybe_finish_planner(%{use_planner: false} = state), do: state
defp maybe_finish_planner(%{ai: ai, use_planner: true, messages: msgs, tools: tools} = state) do
log_tool_call(state, "Consolidating lessons learned from the research")
case AI.Agent.Planner.get_response(ai, %{msgs: msgs, tools: tools, stage: :finish}) do
{:ok, response} ->
planner_msg = AI.Util.system_msg(response)
%__MODULE__{state | messages: state.messages ++ [planner_msg]}
{:error, reason} ->
log_tool_call_error(state, "planner", reason)
state
end
end
# -----------------------------------------------------------------------------
# Tool calls
# -----------------------------------------------------------------------------
defp handle_tool_calls(%{tool_call_requests: tool_calls} = state) do
{:ok, queue} = Queue.start_link(&handle_tool_call(state, &1))
outputs =
tool_calls
|> Queue.map(queue)
|> Enum.flat_map(fn {:ok, msgs} -> msgs end)
Queue.shutdown(queue)
Queue.join(queue)
%__MODULE__{
state
| tool_call_requests: [],
messages: state.messages ++ outputs
}
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
response = AI.Util.tool_msg(id, func, output)
{:ok, [request, response]}
else
:error ->
on_event(state, :tool_call_error, {func, args_json, :error})
msg = "An error occurred (most likely incorrect arguments)"
response = AI.Util.tool_msg(id, func, msg)
{:ok, [request, response]}
{:error, reason} ->
on_event(state, :tool_call_error, {func, args_json, {:error, reason}})
response = AI.Util.tool_msg(id, func, reason)
{:ok, [request, response]}
{:error, :unknown_tool, tool} ->
on_event(state, :tool_call_error, {func, args_json, {:error, "Unknown tool: #{tool}"}})
error = """
Your attempt to call #{func} failed because the tool '#{tool}' is unknown.
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} ->
on_event(
state,
:tool_call_error,
{func, args_json, {:error, "Missing required argument: #{key}"}}
)
spec =
with {:ok, spec} <- AI.Tools.tool_spec(func),
{: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]}
end
end
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 ->
on_event(state, :tool_call, {func, args})
result =
AI.Tools.perform_tool_call(state, func, args)
|> case do
{:ok, response} when is_binary(response) -> {:ok, response}
{:ok, response} -> Jason.encode(response)
:ok -> {:ok, "#{func} completed successfully"}
other -> other
end
on_event(state, :tool_call_result, {func, args, result})
result
end)
end
end
# -----------------------------------------------------------------------------
# Tool call UI integration
# -----------------------------------------------------------------------------
defp log_user_msg(state, msg) do
if state.log_msgs do
UI.info("You", msg)
end
end
defp log_assistant_msg(state, msg) do
if state.log_msgs do
UI.info("Assistant", msg)
end
end
defp log_tool_call(state, step) do
if state.log_tool_calls do
UI.info(step)
end
end
defp log_tool_call(state, step, msg) do
if state.log_tool_calls do
UI.info(step, msg)
end
end
defp log_tool_call_result(state, step) do
if state.log_tool_call_results do
UI.debug(step)
end
end
defp log_tool_call_result(state, step, msg) do
if state.log_tool_call_results do
UI.debug(step, msg)
end
end
defp log_tool_call_error(_state, tool, reason) do
UI.error("Error calling #{tool}", reason)
end
# -----------------------------------------------------------------------------
# Tool call logging
# -----------------------------------------------------------------------------
defp on_event(state, :tool_call, {tool, args}) do
AI.Tools.with_args(tool, args, fn args ->
AI.Tools.on_tool_request(tool, args)
|> case do
nil -> state
{step, msg} -> log_tool_call(state, step, msg)
step -> log_tool_call(state, step)
end
end)
end
defp on_event(state, :tool_call_result, {tool, args, {:ok, result}}) do
AI.Tools.with_args(tool, args, fn args ->
AI.Tools.on_tool_result(tool, args, result)
|> case do
nil -> state
{step, msg} -> log_tool_call_result(state, step, msg)
step -> log_tool_call_result(state, step)
end
end)
end
defp on_event(state, :tool_call_error, {tool, _args_json, {:error, reason}}) do
reason =
if is_binary(reason) do
reason
else
inspect(reason, pretty: true)
end
log_tool_call_error(state, tool, reason)
end
defp on_event(_state, _, _), do: :ok
# ----------------------------------------------------------------------------
# Continuing a conversation
# ----------------------------------------------------------------------------
defp replay_conversation(%{replay_conversation: false} = state), do: state
defp replay_conversation(state) do
messages = Util.string_keys_to_atoms(state.messages)
# Make a lookup for tool call args by id
tool_call_args =
messages
|> Enum.reduce(%{}, fn msg, acc ->
case msg do
%{role: "assistant", content: nil, tool_calls: tool_calls} ->
tool_calls
|> Enum.map(fn %{id: id, function: %{arguments: args}} -> {id, args} end)
|> Enum.into(acc)
_ ->
acc
end
end)
messages
# Skip the first message, which is the system prompt for the agent
|> Enum.drop(1)
|> Enum.each(fn
%{role: "assistant", content: nil, tool_calls: tool_calls} ->
tool_calls
|> Enum.each(fn %{function: %{name: func, arguments: args_json}} ->
with {:ok, args} <- Jason.decode(args_json) do
on_event(state, :tool_call, {func, args})
end
end)
%{role: "tool", name: func, tool_call_id: id, content: content} ->
on_event(state, :tool_call_result, {func, tool_call_args[id], content})
%{role: "system", content: content} ->
on_event(state, :tool_call_result, {"planner", %{}, content})
%{role: "assistant", content: content} ->
log_assistant_msg(state, content)
%{role: "user", content: content} ->
log_user_msg(state, content)
end)
state
end
end