Packages

fnord

0.4.13

AI code archaeology

Current section

Files

Jump to
fnord lib ask.ex
Raw

lib/ask.ex

defmodule Ask do
defstruct [
:opts,
:ai,
:question,
:matched_files,
:tool_call,
:buffer,
:last_chunk,
:messages
]
@tool_call_acc %{
id: "",
func: "",
args: "",
output: ""
}
def new(opts) do
%Ask{
opts: opts,
ai: AI.new(),
question: opts.question,
matched_files: [],
tool_call: @tool_call_acc,
buffer: "",
last_chunk: "",
messages: [
AI.system_message(),
AI.user_message(opts.question)
]
}
end
def run(ask) do
ask
|> start_output()
|> send_request()
|> end_output()
end
def reset_buffers(ask) do
%Ask{ask | buffer: "", last_chunk: "", tool_call: @tool_call_acc}
end
defp start_output(%Ask{opts: %{quiet: true}} = ask) do
ask
end
defp start_output(ask) do
Owl.LiveScreen.add_block(:answer, state: "Assistant is thinking...")
ask
end
defp output(%Ask{opts: %{quiet: true}, last_chunk: data} = ask) do
IO.write(data)
ask
end
defp output(ask) do
Owl.LiveScreen.update(:answer, ask.buffer)
Owl.LiveScreen.await_render()
ask
end
defp end_output(%Ask{opts: %{quiet: true}} = ask) do
IO.puts("")
ask
end
defp end_output(ask) do
Owl.IO.puts("")
ask
end
# -----------------------------------------------------------------------------
# Stream processing
# -----------------------------------------------------------------------------
def send_request(%{buffer: "", tool_call: @tool_call_acc} = ask) do
ask.ai
|> AI.stream(ask.messages)
|> process_stream(ask)
end
def send_request(ask) do
ask.ai
|> AI.stream(ask.messages)
|> process_stream(ask |> reset_buffers())
end
defp process_stream(stream, ask) do
stream
|> Stream.flat_map(& &1)
|> Enum.reduce(ask, fn event, ask ->
handle_response(ask, event)
end)
end
# -----------------------------------------------------------------------------
# General event handling
# -----------------------------------------------------------------------------
# Strip out the :data and "choices" wrappers
defp handle_response(ask, %{data: %{"choices" => event}}) do
handle_response(ask, event)
end
# -----------------------------------------------------------------------------
# Message events
# -----------------------------------------------------------------------------
# The message is complete
defp handle_response(ask, [%{"finish_reason" => "stop"}]) do
%Ask{ask | messages: ask.messages ++ [AI.assistant_message(ask.buffer)]}
|> reset_buffers()
end
# Extract the message content
defp handle_response(ask, [%{"delta" => %{"content" => content}}])
when not is_nil(content) do
%Ask{ask | last_chunk: content, buffer: ask.buffer <> content}
|> output()
end
# -----------------------------------------------------------------------------
# Tool call events
# -----------------------------------------------------------------------------
defp handle_response(ask, [
%{
"delta" => %{
"tool_calls" => [
%{"id" => id, "function" => %{"name" => name}}
]
}
}
]) do
tool_call =
ask.tool_call
|> Map.put(:id, id)
|> Map.put(:func, name)
%Ask{ask | tool_call: tool_call}
end
# Collect tool call fragments (both "name" and "arguments")
defp handle_response(ask, [%{"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", "")
# Accumulate the fragments into the tool call
tool_call =
ask.tool_call
|> Map.update!(:func, &(&1 <> name_frag))
|> Map.update!(:args, &(&1 <> args_frag))
%Ask{ask | tool_call: tool_call}
end
# Handle the completion of tool calls
defp handle_response(ask, [%{"finish_reason" => "tool_calls"}]) do
with {:ok, output} <- handle_tool_call(ask) do
tool_request =
AI.assistant_tool_message(
ask.tool_call.id,
ask.tool_call.func,
ask.tool_call.args
)
tool_response = AI.tool_message(ask.tool_call.id, ask.tool_call.func, output)
messages = ask.messages ++ [tool_request, tool_response]
%Ask{ask | messages: messages, tool_call: %{ask.tool_call | output: output}}
|> send_request()
end
end
# Catch-all for unhandled events
defp handle_response(ask, event) do
IO.inspect(event, label: "UNHANDLED")
ask
end
# -----------------------------------------------------------------------------
# Tool call outputs
# -----------------------------------------------------------------------------
# Function to execute the tool call
defp handle_tool_call(%{tool_call: %{func: "search_tool", args: args_json}} = ask) do
with {:ok, args} <- Jason.decode(args_json),
{:ok, query} <- Map.fetch(args, "query") do
ask.opts
|> Map.put(:concurrency, ask.opts.concurrency)
|> Map.put(:detail, true)
|> Map.put(:limit, 10)
|> Map.put(:query, query)
|> Search.new()
|> Search.get_results()
|> Enum.map(fn {file, score, data} ->
"""
-----
# File: #{file} | Score: #{score}
## Summary
#{data["summary"]}
## Contents
```
#{data["contents"]}
```
"""
end)
|> Enum.join("\n")
|> then(&{:ok, &1})
else
{:error, reason} -> {:error, reason}
end
end
defp handle_tool_call(_ask) do
{:error, :unhandled_tool_call}
end
end