Packages
fnord
0.4.7
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/chat.ex
defmodule Chat do
defstruct [
:opts,
:ai,
:settings,
:assistant_id,
:next_asst_msg_box_id
]
@assistant_version "v1.0.0"
@assistant_name "Fnord Prefect"
@assistant_model "gpt-4o"
@assistant_prompt """
You are a conversational interface to a database of information about the
user's project. The database may contain:
### Code files:
- **Synopsis**
- **Languages present in the file**
- **Business logic and behaviors**
- **List of symbols**
- **Map of calls to other modules**
### Documentation files (e.g., README, wiki pages, general documentation):
- **Synopsis**: A brief overview of what the document covers.
- **Topics and Sections**: A list of main topics or sections in the document.
- **Definitions and Key Terms**: Any specialized terms or jargon defined in the document.
- **Links and References**: Important links or references included in the document.
- **Key Points and Highlights**: Main points or takeaways from the document.
The user will prompt you with a question. You will use your `search_tool` to
search the database in order to gain enough knowledge to answer the question
as completely as possible. It may require multiple searches before you have
all of the information you need.
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.
"""
@search_tool %{
type: "function",
function: %{
name: "search_tool",
description: "searches for matching files and their contents",
parameters: %{
type: "object",
properties: %{
query: %{
type: "string",
description: "The search query string."
}
},
required: ["query"]
}
}
}
@assistant_request OpenaiEx.Beta.Assistants.new(%{
name: @assistant_name,
instructions: @assistant_prompt,
model: @assistant_model,
tools: [@search_tool],
metadata: %{version: @assistant_version}
})
@doc """
Create a new chat instance.
"""
def new(opts) do
ai = AI.new()
settings = Settings.new()
assistant_id = Settings.get(settings, "assistant_id")
%{"id" => assistant_id} = get_assistant(assistant_id, ai)
Settings.set(settings, :assistant_id, assistant_id)
%Chat{
opts: opts,
ai: ai,
settings: settings,
assistant_id: assistant_id
}
end
# -----------------------------------------------------------------------------
# Chat loop
# -----------------------------------------------------------------------------
@doc """
Runs the chat loop. Exits on Control-C.
"""
def run(chat) do
if is_nil(chat.next_asst_msg_box_id) do
Owl.IO.puts(Owl.Data.tag("Chatting with the assistant. Control-C exits.", [:cyan, :bright]))
end
chat_loop(chat)
end
defp chat_loop(chat) do
with {:ok, %{"id" => thread_id}} <- AI.start_thread(chat.ai) do
# Get the user's message and add it to the thread.
message = user_message_prompt(chat)
# Add the user's message to the tui message list.
add_user_message(chat, message)
next_asst_msg_box_id = add_assistant_message(chat, "Sending your message...")
chat = %Chat{chat | next_asst_msg_box_id: next_asst_msg_box_id}
# Attach the user's message and run the thread
with {:ok, _} <- AI.add_user_message(chat.ai, thread_id, message),
{:ok, :done} <- run_thread(chat, thread_id),
{:ok, msgs} <- AI.get_messages(chat.ai, thread_id) do
msgs
|> Map.get("data", [])
|> Enum.filter(fn %{"role" => role} -> role == "assistant" end)
|> Enum.each(fn %{"content" => [%{"text" => %{"value" => message}}]} ->
update_assistant_message(chat, message)
end)
end
end
Process.sleep(200)
chat_loop(chat)
end
# -----------------------------------------------------------------------------
# Threads and runs
# -----------------------------------------------------------------------------
defp run_thread(chat, thread_id) do
with {:ok, %{"id" => run_id}} <- AI.run_thread(chat.ai, chat.assistant_id, thread_id) do
run_thread(chat, thread_id, run_id)
end
end
defp run_thread(chat, thread_id, run_id) do
case AI.get_run_status(chat.ai, thread_id, run_id) do
{:ok, %{"status" => "queued"}} ->
update_assistant_message(chat, "Assistant is typing...")
run_thread(chat, thread_id, run_id)
{:ok, %{"status" => "in_progress"}} ->
update_assistant_message(chat, "Assistant is typing...")
run_thread(chat, thread_id, run_id)
# Requires action; most likely a tool call request
{:ok, %{"status" => "requires_action"} = status} ->
update_assistant_message(chat, "Researching project...")
outputs = get_tool_outputs(chat, status)
case AI.submit_tool_outputs(chat.ai, thread_id, run_id, outputs) do
{:ok, _} -> run_thread(chat, thread_id, run_id)
{:error, reason} -> {:error, reason}
end
{:ok, %{"status" => "completed"}} ->
update_assistant_message(chat, "Formatting response...")
{:ok, :done}
{:ok, status} ->
update_assistant_message(chat, "Error! API response:\n\n#{inspect(status)}")
{:error, :failed}
{:error, reason} ->
update_assistant_message(chat, "Error! API response:\n\n#{inspect(reason)}")
{:error, reason}
end
end
# -----------------------------------------------------------------------------
# Tool calls
# -----------------------------------------------------------------------------
defp get_tool_outputs(chat, run_status) do
run_status
|> Map.get("required_action", %{})
|> Map.get("submit_tool_outputs", %{})
|> Map.get("tool_calls", [])
|> Enum.map(&get_tool_call_output(chat, &1))
|> Enum.reject(&is_nil/1)
end
defp get_tool_call_output(
chat,
%{
"id" => id,
"function" => %{
"name" => "search_tool",
"arguments" => json_args_string
}
}
) do
%{"query" => query} = Jason.decode!(json_args_string)
update_assistant_message(chat, "Searching for: #{query}")
{:ok, results} =
chat.opts
|> Map.put(:detail, true)
|> Map.put(:limit, 5)
|> Map.put(:query, query)
|> Search.new()
|> Search.get_results()
|> Enum.map(fn {file, score, data} ->
"""
-----
# File: #{file} | Score: #{score}
#{data["summary"]}
"""
end)
|> Enum.join("\n")
|> then(fn res -> {:ok, res} end)
%{tool_call_id: id, output: results}
end
defp get_tool_call_output(_chat, _status), do: nil
# -----------------------------------------------------------------------------
# Creating and maintaining the assistant
# -----------------------------------------------------------------------------
defp get_assistant(nil, ai) do
AI.create_assistant(ai, @assistant_request)
end
defp get_assistant(assistant_id, ai) do
AI.get_assistant(ai, assistant_id)
|> case do
{:ok, assistant} -> update_assistant(assistant, ai)
_ -> get_assistant(nil, ai)
end
end
defp update_assistant(assistant, ai) do
if assistant["metadata"]["version"] != @assistant_version do
AI.update_assistant(ai, assistant["id"], @assistant_request)
else
assistant
end
end
# -----------------------------------------------------------------------------
# UI
# -----------------------------------------------------------------------------
defp user_message_prompt(_chat) do
Owl.IO.input(label: "Enter your message:")
end
defp add_user_message(_chat, message) do
Owl.LiveScreen.add_block(
Base.encode16(:crypto.strong_rand_bytes(8)),
state:
Owl.Box.new(message,
title: " You ",
border_style: :solid_rounded,
border_tag: :green,
padding: 1,
min_height: 1,
min_width: 140,
max_width: 140,
horizontal_align: :left,
vertical_align: :top,
word_wrap: :normal
)
)
end
defp add_assistant_message(_chat, message) do
id = Base.encode16(:crypto.strong_rand_bytes(8))
Owl.LiveScreen.add_block(id,
state:
Owl.Box.new(message,
title: " Assistant ",
border_style: :solid_rounded,
border_tag: :cyan,
padding: 1,
min_height: 1,
min_width: 140,
max_width: 140,
horizontal_align: :left,
vertical_align: :top,
word_wrap: :normal
)
)
id
end
defp update_assistant_message(chat, message) do
Owl.LiveScreen.update(
chat.next_asst_msg_box_id,
Owl.Box.new(message,
title: " Assistant ",
border_style: :solid_rounded,
border_tag: :cyan,
padding: 1,
min_height: 1,
min_width: 140,
max_width: 140,
horizontal_align: :left,
vertical_align: :top,
word_wrap: :normal
)
)
Owl.LiveScreen.await_render()
end
end