Packages
fnord
0.4.16
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/answers.ex
defmodule AI.Agent.Answers do
@moduledoc """
This module provides an agent that answers questions by searching a database
of information about the user's project. It uses a search tool to find
matching files and their contents in order to generate a complete and concise
answer for the user.
"""
defstruct([
:ai,
:opts,
:callback,
:last_msg_chunk,
:msg_buffer,
:tool_calls,
:messages
])
@model "gpt-4o"
@prompt """
You are a conversational interface to a database of information about the
user's project.
Your database may contain:
- Code: synopsis, languages, business logic, symbols, and external calls
- Docs: synopsis, topics, definitions, links, references, key points, and highlights
Tools available to you:
- list_files_tool: List all files in the project database
- search_tool: Search for phrases in the project embeddings database as many
times as you need to ensure you have all of the context required to answer
the user's question fully
- file_question_tool: Ask an AI agent to answer a specific question about a
file in the project
Once you have all of the information you need, provide the user with a
complete yet concise answer, including generating any requested code or
producing on-demand documentation by assimilating the information you have
gathered.
By default, answer as tersely as possible. Increase your verbosity in
proportion to the specificity of the question.
ALWAYS finish your response with a list of the relevant files that you found.
Exclude files that are not relevant to the user's question. Format them as a
list, where each file name is bolded and is followed by a colon and an
explanation of how it is relevant. Err on the side of inclusion if you are
unsure.
"""
@tool_call %{
id: nil,
func: "",
args: ""
}
def new(ai, opts, callback) do
%AI.Agent.Answers{
ai: ai,
opts: opts,
callback: callback,
last_msg_chunk: "",
msg_buffer: "",
tool_calls: [],
messages: [
system_message(),
user_message(opts.question)
]
}
end
def perform(agent) do
send_request(agent)
end
defp reset_buffers(agent) do
%AI.Agent.Answers{agent | msg_buffer: "", last_msg_chunk: "", tool_calls: []}
end
# -----------------------------------------------------------------------------
# Stream processing
# -----------------------------------------------------------------------------
defp send_request(agent) do
Ask.update_status("Talking to the assistant")
agent.ai
|> get_response_stream(agent.messages)
|> process_stream(agent |> reset_buffers())
end
defp get_response_stream(ai, messages) do
chat_req =
OpenaiEx.Chat.Completions.new(
model: @model,
tool_choice: "auto",
messages: messages,
tools: [
AI.Tools.Search.spec(),
AI.Tools.ListFiles.spec(),
AI.Tools.FileQuestion.spec()
]
)
{:ok, chat_stream} =
OpenaiEx.Chat.Completions.create(
ai.client,
chat_req,
stream: true
)
chat_stream.body_stream
end
defp process_stream(stream, agent) do
stream
|> Stream.flat_map(& &1)
|> Stream.map(fn %{data: %{"choices" => [event]}} -> event end)
|> Enum.reduce(agent, fn event, agent ->
handle_response(agent, event)
end)
end
# -----------------------------------------------------------------------------
# Message events
# -----------------------------------------------------------------------------
# The message is complete
defp handle_response(agent, %{"finish_reason" => "stop"}) do
Ask.update_status("Answer received")
%AI.Agent.Answers{agent | messages: agent.messages ++ [assistant_message(agent.msg_buffer)]}
|> reset_buffers()
end
# Extract the message content
defp handle_response(agent, %{"delta" => %{"content" => content}})
when is_binary(content) do
Ask.update_status("Assistant is typing...")
agent = %AI.Agent.Answers{
agent
| last_msg_chunk: content,
msg_buffer: agent.msg_buffer <> content
}
agent.callback.(content, agent.msg_buffer)
agent
end
# -----------------------------------------------------------------------------
# Tool call events
# -----------------------------------------------------------------------------
# THe initial response contains the function and tool call ID
defp handle_response(agent, %{
"delta" => %{
"tool_calls" => [
%{"id" => id, "function" => %{"name" => name}}
]
}
}) do
Ask.update_status("Assistant is preparing a tool call: #{name}")
tool_call =
@tool_call
|> Map.put(:id, id)
|> Map.put(:func, name)
tool_calls = [tool_call | agent.tool_calls]
%AI.Agent.Answers{agent | tool_calls: tool_calls}
end
# Collect tool call fragments (both "name" and "arguments")
defp handle_response(agent, %{"delta" => %{"tool_calls" => [%{"function" => frag}]}}) do
# Extract fragments of "id", "name", and "arguments" if they exist
name_frag = Map.get(frag, "name", "")
args_frag = Map.get(frag, "arguments", "")
{[tool_call], tool_calls} = Enum.split(agent.tool_calls, 1)
# Accumulate the fragments into the tool call
tool_call =
tool_call
|> Map.update!(:func, &(&1 <> name_frag))
|> Map.update!(:args, &(&1 <> args_frag))
%AI.Agent.Answers{agent | tool_calls: [tool_call | tool_calls]}
end
# Handle the completion of tool calls
defp handle_response(agent, %{"finish_reason" => "tool_calls"}) do
agent
|> handle_tool_calls()
|> send_request()
end
# -----------------------------------------------------------------------------
# Catch-all for unhandled events
# -----------------------------------------------------------------------------
defp handle_response(agent, _event) do
agent
end
# -----------------------------------------------------------------------------
# Tool calls
# -----------------------------------------------------------------------------
defp handle_tool_calls(%{tool_calls: []} = agent) do
agent
end
defp handle_tool_calls(%{tool_calls: [tool_call | remaining]} = agent) do
with {:ok, agent} <- handle_tool_call(agent, tool_call) do
%AI.Agent.Answers{agent | tool_calls: remaining}
|> handle_tool_calls()
end
end
defp handle_tool_call(agent, %{id: id, func: func, args: args_json}) do
with {:ok, args} <- Jason.decode(args_json),
{:ok, output} <- perform_tool_call(agent, func, args) do
request = assistant_tool_message(id, func, args_json)
response = tool_message(id, func, output)
{:ok, %AI.Agent.Answers{agent | messages: agent.messages ++ [request, response]}}
end
end
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)
end
defp perform_tool_call(agent, "list_files_tool", args) do
AI.Tools.ListFiles.call(agent, args)
end
defp perform_tool_call(agent, "file_question_tool", args) do
AI.Tools.FileQuestion.call(agent, args)
end
defp perform_tool_call(_agent, func, _args) do
{:error, :unhandled_tool_call, func}
end
# -----------------------------------------------------------------------------
# Message construction
# -----------------------------------------------------------------------------
defp system_message(), do: OpenaiEx.ChatMessage.system(@prompt)
defp assistant_message(msg), do: OpenaiEx.ChatMessage.assistant(msg)
defp user_message(msg), do: OpenaiEx.ChatMessage.user(msg)
defp tool_message(id, func, output), do: OpenaiEx.ChatMessage.tool(id, func, output)
defp assistant_tool_message(id, func, args) do
%{
role: "assistant",
content: nil,
tool_calls: [
%{
id: id,
type: "function",
function: %{
name: func,
arguments: args
}
}
]
}
end
end