Packages
fnord
0.4.25
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/agent/spelunker.ex
defmodule AI.Agent.Spelunker do
defstruct [
:ai,
:opts,
:symbol,
:start_file,
:question,
:messages,
:response,
:requested_tool_calls
]
@model "gpt-4o"
@prompt """
You are the Spelunker Agent. Your job is to *thoroughly* work through maps of code symbols and function calls to *completely* trace paths through the code base.
You are a code explorer and a graph search, digging through "outlines" (representations of code files as symbols and their relationships) to trace paths through the code base on behalf of the Answers Agent, who interacts with the user.
You will assist the Answers Agent in answering questions about the code base by following the path from one symbol to another or by identifying files and assembling a call back to a particular symbol.
Use the tool calls at your disposal to dig through the code base; combine multiple tool calls into a single request to perform them concurrently.
Use your tools as many times as necessary to ensure that you have the COMPLETE picture. Do NOT respond ambiguously unless you have made multiple attempts to find the answer.
You will use these outlines to navigate code files, tracing paths through the code in order to assist the Answers Agent in correctly answering the user's questions about the code base.
To trace calls between two points, start with the target symbol and work your backwards to the starting symbol, or use a binary search strategy, retrieving outlines from both ends and working your way toward one another.
Your highest priority is to provide COMPLETE and ACCURATE information to the Answers Agent; ensure you have a complete code path before sending your response.
"""
def new(ai, opts, symbol, start_file, question) do
%__MODULE__{
ai: ai,
opts: opts,
symbol: symbol,
start_file: start_file,
question: question,
messages: [
OpenaiEx.ChatMessage.system(@prompt),
OpenaiEx.ChatMessage.user("""
The Answers Agent has requested your assistance in tracing a path
through the code base, beginning with the symbol `#{symbol}` in the
file `#{start_file}`, in order to discover the answer to this question:
`#{question}`.
""")
],
requested_tool_calls: []
}
end
def trace(agent) do
agent
|> send_request()
|> then(&{:ok, &1.response})
end
defp send_request(agent) do
agent
|> build_request()
|> get_response(agent)
|> handle_response(agent)
end
defp build_request(agent) do
OpenaiEx.Chat.Completions.new(
model: @model,
tool_choice: "auto",
messages: agent.messages,
tools: [
AI.Tools.Search.spec(),
AI.Tools.ListFiles.spec(),
AI.Tools.FileInfo.spec(),
AI.Tools.Outline.spec()
]
)
end
defp get_response(request, agent) do
completion = OpenaiEx.Chat.Completions.create(agent.ai.client, request)
with {:ok, %{"choices" => [event]}} <- completion do
event
end
end
defp handle_response(%{"finish_reason" => "stop"} = response, agent) do
with %{"message" => %{"content" => content}} <- response do
%__MODULE__{agent | response: content}
end
end
defp handle_response(%{"finish_reason" => "tool_calls"} = response, agent) do
with %{"message" => %{"tool_calls" => tool_calls}} <- response do
%__MODULE__{agent | requested_tool_calls: tool_calls}
|> handle_tool_calls()
|> send_request()
end
end
defp handle_response({:error, %OpenaiEx.Error{message: "Request timed out."}}, agent) do
Tui.warn("Request timed out. Retrying in 500 ms.")
Process.sleep(500)
send_request(agent)
end
defp handle_response({:error, %OpenaiEx.Error{message: msg}}, agent) do
%__MODULE__{
agent
| response: """
I encountered an error while processing your request. Please try again.
The error message was:
#{msg}
"""
}
end
# -----------------------------------------------------------------------------
# Tool calls
# -----------------------------------------------------------------------------
defp handle_tool_calls(%{requested_tool_calls: tool_calls} = agent) do
{:ok, queue} =
Queue.start_link(agent.opts.concurrency, fn tool_call ->
handle_tool_call(agent, tool_call)
end)
outputs =
tool_calls
|> Queue.map(queue)
|> Enum.reduce([], fn
{:ok, msgs}, acc -> acc ++ msgs
_, acc -> acc
end)
Queue.shutdown(queue)
Queue.join(queue)
%__MODULE__{
agent
| requested_tool_calls: [],
messages: agent.messages ++ outputs
}
end
def handle_tool_call(
agent,
%{
"id" => id,
"function" => %{
"name" => func,
"arguments" => args_json
}
}
) do
with {:ok, args} <- Jason.decode(args_json),
{:ok, output} <- perform_tool_call(agent, func, args) do
request = AI.Util.assistant_tool_msg(id, func, args_json)
response = AI.Util.tool_msg(id, func, output)
{:ok, [request, response]}
else
error ->
Tui.warn("Error handling tool call | tool=#{func} args=#{args_json}", inspect(error))
{:error, []}
error
end
end
# -----------------------------------------------------------------------------
# Tool call outputs
# -----------------------------------------------------------------------------
defp perform_tool_call(agent, func, args_json) when is_binary(args_json) do
with {:ok, args} <- Jason.decode(args_json) do
perform_tool_call(agent, func, args)
end
end
defp perform_tool_call(agent, "search_tool", args), do: AI.Tools.Search.call(agent, args)
defp perform_tool_call(agent, "list_files_tool", args), do: AI.Tools.ListFiles.call(agent, args)
defp perform_tool_call(agent, "file_info_tool", args), do: AI.Tools.FileInfo.call(agent, args)
defp perform_tool_call(agent, "outline_tool", args), do: AI.Tools.Outline.call(agent, args)
defp perform_tool_call(_agent, func, _args), do: {:error, :unhandled_tool_call, func}
end