Packages
fnord
0.4.43
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/response.ex
defmodule AI.Response do
@moduledoc """
This module sends a request to the model and handles the response. It is able
to handle tool calls and responses. If the caller includes an `on_event`
function, it will be called whenever a tool call is performed or if a tool
call results in an error.
"""
@type response :: {
:ok,
String.t(),
context_window_usage
}
@type context_window_usage :: {
# "Context window usage"
String.t(),
# "50.00% | 500 / 1000"
String.t()
}
@spec get(AI.t(), keyword) :: response
def get(ai, opts) do
with {:ok, max_tokens} <- Keyword.fetch(opts, :max_tokens),
{:ok, model} <- Keyword.fetch(opts, :model),
{:ok, system} <- Keyword.fetch(opts, :system),
{:ok, user} <- Keyword.fetch(opts, :user) do
tools = Keyword.get(opts, :tools, nil)
use_planner = Keyword.get(opts, :use_planner, false)
log_tool_calls = Keyword.get(opts, :log_tool_calls, false)
log_tool_call_results = Keyword.get(opts, :log_tool_call_results, false)
%{
ai: ai,
opts: Enum.into(opts, %{}),
max_tokens: max_tokens,
model: model,
use_planner: use_planner,
tools: tools,
log_tool_calls: log_tool_calls,
log_tool_call_results: log_tool_call_results,
messages: [AI.Util.system_msg(system), AI.Util.user_msg(user)],
tool_call_requests: [],
response: nil
}
|> send_request()
|> then(fn state ->
{:ok, state.response, context_window_usage(state)}
end)
end
end
defp 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
# -----------------------------------------------------------------------------
# Response handling
# -----------------------------------------------------------------------------
defp send_request(state) do
state
|> maybe_use_planner()
|> get_completion()
|> handle_response(state)
end
def get_completion(state) do
AI.get_completion(state.ai, state.model, state.messages, state.tools)
end
defp handle_response({:ok, :msg, response}, state) do
%{state | 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, reason}, state) do
reason =
if is_binary(reason) do
reason
else
inspect(reason)
end
%{
state
| response: """
I encountered an error while processing your request.
The error message was:
#{reason}
"""
}
end
# -----------------------------------------------------------------------------
# Planner
# -----------------------------------------------------------------------------
defp maybe_use_planner(%{use_planner: false} = state) do
state
end
defp maybe_use_planner(%{ai: ai, use_planner: true, messages: msgs, tools: tools} = state) do
on_event(state, :tool_call, {"planner", %{}})
case AI.Agent.Planner.get_response(ai, %{msgs: msgs, tools: tools}) do
{:ok, response, _usage} ->
on_event(state, :tool_call_result, {"planner", %{}, response})
planner_msg = AI.Util.system_msg(response)
%{state | messages: state.messages ++ [planner_msg]}
{:error, reason} ->
on_event(state, :tool_call_error, {"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)
%{
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)
UI.debug("Tool call", "#{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, reason} ->
on_event(state, :tool_call_error, {func, args_json, reason})
response = AI.Util.tool_msg(id, func, reason)
{: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
on_event(state, :tool_call, {func, args})
result = AI.Tools.perform_tool_call(state, func, args)
on_event(state, :tool_call_result, {func, args, result})
result
end
end
# -----------------------------------------------------------------------------
# Tool call UI integration
# -----------------------------------------------------------------------------
defp log_tool_call(state, step) do
if state.log_tool_calls do
UI.begin_step(step)
end
end
defp log_tool_call(state, step, msg) do
if state.log_tool_calls do
UI.begin_step(step, msg)
end
end
defp log_tool_call_result(state, step, msg) do
if state.log_tool_call_results do
UI.end_step(step, msg)
end
end
defp log_tool_call_error(_state, tool, reason) do
UI.error("Error calling #{tool}", reason)
end
# ----------------------------------------------------------------------------
# Planner
# ----------------------------------------------------------------------------
defp on_event(state, :tool_call, {"planner", _}) do
log_tool_call(state, "Planning next steps")
end
defp on_event(state, :tool_call_result, {"planner", _, plan}) do
log_tool_call_result(state, "Next steps", plan)
end
# ----------------------------------------------------------------------------
# Search tool
# ----------------------------------------------------------------------------
defp on_event(state, :tool_call, {"search_tool", args}) do
log_tool_call(state, "Searching", args["query"])
end
# ----------------------------------------------------------------------------
# List files tool
# ----------------------------------------------------------------------------
defp on_event(state, :tool_call, {"list_files_tool", _}) do
log_tool_call(state, "Listing files in project")
end
# ----------------------------------------------------------------------------
# File info tool
# ----------------------------------------------------------------------------
defp on_event(state, :tool_call, {"file_info_tool", args}) do
log_tool_call(state, "Considering #{args["file"]}", args["question"])
end
defp on_event(state, :tool_call_result, {"file_info_tool", args, {:ok, response}}) do
log_tool_call_result(
state,
"Considered #{args["file"]}",
"#{args["question"]}\n\n#{response}"
)
end
# ----------------------------------------------------------------------------
# Spelunker tool
# ----------------------------------------------------------------------------
defp on_event(state, :tool_call, {"spelunker_tool", args}) do
msg = "#{args["start_file"]} | #{args["symbol"]}: #{args["question"]}"
log_tool_call(state, "Spelunking", msg)
end
defp on_event(state, :tool_call_result, {"spelunker_tool", args, {:ok, response}}) do
msg = "#{args["start_file"]} | #{args["symbol"]}: #{args["question"]}\n\n#{response}"
log_tool_call_result(state, "Spelunked", msg)
end
# ----------------------------------------------------------------------------
# Git tools
# ----------------------------------------------------------------------------
defp on_event(state, :tool_call, {"git_show_tool", args}) do
log_tool_call(state, "Inspecting commit", args["sha"])
end
defp on_event(state, :tool_call, {"git_pickaxe_tool", args}) do
log_tool_call(state, "Archaeologizing", args["regex"])
end
defp on_event(state, :tool_call, {"git_diff_branch_tool", args}) do
log_tool_call(state, "Diffing branches", "#{args["base"]}..#{args["topic"]}")
end
# ----------------------------------------------------------------------------
# Fallbacks and error messages
# ----------------------------------------------------------------------------
defp on_event(state, :tool_call_error, {tool, _, reason}) do
log_tool_call_error(state, tool, reason)
end
defp on_event(_state, _, _), do: :ok
end