Packages
fnord
0.4.12
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/ask.ex
defmodule Ask do
defstruct [
:opts,
:settings,
:assistant_id,
:thread_id,
:run_id,
:status_msgs,
:assistant_searches,
:files_found,
:response
]
@last_thread_id_setting "last_thread_id"
def new(opts) do
%Ask{
opts: opts,
settings: Settings.new(),
status_msgs: [],
assistant_searches: [],
files_found: []
}
end
def run(opts) do
ask = new(opts)
if opts[:quiet] do
do_run(ask)
else
Spinner.run(fn -> do_run(ask) end, "Assistant is responding")
end
end
defp do_run(ask) do
with {:ok, ask} <- with_assistant_id(ask),
{:ok, ask} <- with_thread_id(ask),
{:ok, ask} <- send_prompt(ask),
{:ok, ask} <- with_run_id(ask),
{:ok, ask} <- get_response(ask) do
render(ask)
else
{:error, reason} ->
IO.puts(:stderr, "[fnord] #{inspect(reason)}")
end
end
defp info(ask, msg) do
statuses =
case ask.status_msgs do
[^msg | _] ->
ask.status_msgs
_ ->
if ask.opts[:debug] do
IO.puts(:stderr, "[fnord] " <> msg)
end
[msg | ask.status_msgs]
end
%Ask{ask | status_msgs: statuses}
end
defp render(ask) do
searches =
ask.assistant_searches
|> Enum.reverse()
|> Enum.map(fn query -> "- `#{query}`" end)
|> Enum.join("\n")
files =
ask.files_found
|> Enum.uniq()
|> Enum.reverse()
|> Enum.map(fn file -> "- `#{file}`" end)
|> Enum.join("\n")
if searches != "" do
IO.puts("# Searched")
IO.puts(searches)
IO.puts("")
end
if files != "" do
IO.puts("# Matches")
IO.puts(files)
IO.puts("")
end
if searches != "" and files != "" do
IO.puts("# Response")
end
IO.puts(ask.response)
end
defp send_prompt(ask) do
with {:ok, prompt} <- get_prompt(ask),
{:ok, _} <- AI.add_user_message(ask.thread_id, prompt) do
{:ok, ask}
end
end
defp get_response(ask) do
with {ask, {:ok, :done}} <- run_thread(ask),
{:ok, msg} <- get_last_message(ask) do
msg
|> Map.get(:data, [])
|> Enum.filter(fn %{"role" => role} -> role == "assistant" end)
|> Enum.map(fn %{"content" => [%{"text" => %{"value" => msg}}]} -> msg end)
|> Enum.join("\n\n")
|> then(fn msg -> {:ok, %Ask{ask | response: msg}} end)
end
end
defp get_last_message(ask) do
AI.get_messages(ask.thread_id, limit: 1, order: "desc")
end
defp get_prompt(%Ask{opts: %{question: question}}), do: {:ok, question}
defp run_thread(ask) do
case AI.get_thread_run(ask.thread_id, ask.run_id) do
{:ok, %{status: "queued"}} ->
ask
|> info("Assistant is working")
|> run_thread()
{:ok, %{status: "in_progress"}} ->
ask
|> info("Assistant is working")
|> run_thread()
# Requires action; most likely a tool call request
{:ok, %{status: "requires_action"} = status} ->
{ask, outputs} =
ask
|> info("Searching project")
|> get_tool_outputs(status)
case AI.submit_tool_outputs(ask.thread_id, ask.run_id, outputs) do
{:ok, _} -> run_thread(ask)
{:error, reason} -> {ask, {:error, reason}}
end
{:ok, %{status: "completed"}} ->
{ask, {:ok, :done}}
{:ok, status} ->
ask
|> info("error! API run status: #{inspect(status)}")
|> then(fn _ -> {ask, {:error, :failed}} end)
{:error, reason} ->
ask
|> info("error! API response: #{inspect(reason)}")
|> then(fn _ -> {ask, {:error, reason}} end)
end
end
defp get_tool_outputs(ask, run_status) do
run_status
|> Map.get(:required_action, %{})
|> Map.get("submit_tool_outputs", %{})
|> Map.get("tool_calls", [])
|> Enum.reduce({ask, []}, fn tool_call, {ask, acc} ->
{ask, output} = get_tool_call_output(ask, tool_call)
{ask, [output | acc]}
end)
|> then(fn {ask, outputs} ->
outputs =
outputs
|> Enum.reverse()
|> Enum.reject(&is_nil/1)
{ask, outputs}
end)
end
defp get_tool_call_output(
ask,
%{
"id" => id,
"function" => %{
"name" => "search_tool",
"arguments" => json_args_string
}
}
) do
%{"query" => query} = Jason.decode!(json_args_string)
ask =
%Ask{ask | assistant_searches: [query | ask.assistant_searches]}
|> info(" - #{query}")
search_query =
ask.opts
|> Map.put(:detail, true)
|> Map.put(:limit, 10)
|> Map.put(:query, query)
{ask, search_results} =
search_query
|> Search.new()
|> Search.get_results()
|> Enum.reduce({ask, []}, fn {file, score, data}, {ask, acc} ->
rounded = Float.round(score, 5)
formatted = :io_lib.format("~7.5f", [rounded]) |> List.to_string()
ask = info(ask, " - <#{formatted}> #{file}")
ask = %Ask{ask | files_found: [file | ask.files_found]}
{ask, [{file, score, data} | acc]}
end)
{:ok, results} =
search_results
|> Enum.map(fn {file, score, data} ->
"""
-----
# File: #{file} | Score: #{score}
#{data["summary"]}
"""
end)
|> Enum.join("\n")
|> then(fn res -> {:ok, res} end)
{ask, %{tool_call_id: id, output: results}}
end
defp get_tool_call_output(_ask, _status), do: nil
defp with_run_id(ask) do
case AI.run_thread(ask.assistant_id, ask.thread_id) do
{:ok, run_id} -> {:ok, %Ask{ask | run_id: run_id}}
{:error, reason} -> {:error, reason}
end
end
defp with_assistant_id(ask) do
with {:ok, assistant} <- Assistant.get(ask.settings) do
{:ok, %Ask{ask | assistant_id: assistant.id}}
end
end
defp with_thread_id(ask) do
with true <- continue_last_thread?(ask),
{:ok, ask} <- with_last_thread_id(ask) do
ask = info(ask, "Continuing last thread: #{ask.thread_id}")
{:ok, ask |> info("Continuing last thread: #{ask.thread_id}")}
else
_ ->
{:ok, ask} = with_new_thread_id(ask)
{:ok, ask |> info("Starting new thread: #{ask.thread_id}")}
end
end
defp continue_last_thread?(%{opts: %{continue: true}}), do: true
defp continue_last_thread?(_), do: false
defp with_last_thread_id(ask) do
case Settings.get(ask.settings, @last_thread_id_setting, nil) do
nil -> {:error, :no_thread_id}
thread_id -> {:ok, %Ask{ask | thread_id: thread_id}}
end
end
defp with_new_thread_id(ask) do
with {:ok, thread_id} <- AI.start_thread() do
Settings.set(ask.settings, @last_thread_id_setting, thread_id)
{:ok, %Ask{ask | thread_id: thread_id}}
end
end
end