Packages
fnord
0.9.17
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/completion/compaction.ex
defmodule AI.Completion.Compaction do
@min_savings_tersified 0.3
@spec compact(AI.Util.msg_list()) :: {:ok, AI.Util.msg_list(), non_neg_integer}
def compact(msgs) do
original_usage =
msgs
|> SafeJson.encode!()
|> AI.PretendTokenizer.guesstimate_tokens()
UI.report_step("[compaction]", "Compacting conversation (~#{original_usage} tokens)")
with {:ok, tersified_msgs} <- tersified(msgs) do
new_usage =
tersified_msgs
|> SafeJson.encode!()
|> AI.PretendTokenizer.guesstimate_tokens()
savings = Float.round((original_usage - new_usage) / original_usage * 100, 2)
UI.report_step("[compaction]", "Savings: #{savings}%")
{:ok, tersified_msgs, new_usage}
else
{:error, reason} ->
UI.warn("[compaction]", "Failed: #{inspect(reason, pretty: true, limit: :infinity)}")
{:ok, msgs, original_usage}
end
end
defp tersified(msgs) do
UI.report_step("[compaction]", "Compacting individual messages")
with {:ok, tersified_msgs} <- tersify(msgs) do
original_usage =
msgs
|> SafeJson.encode!()
|> AI.PretendTokenizer.guesstimate_tokens()
new_usage =
tersified_msgs
|> SafeJson.encode!()
|> AI.PretendTokenizer.guesstimate_tokens()
savings = (original_usage - new_usage) / original_usage
if savings >= @min_savings_tersified do
{:ok, tersified_msgs}
else
pct = Float.round(savings * 100, 2)
UI.report_step("[compaction]", "Savings: #{pct}% - meh, let's try summarizing it")
summarized(tersified_msgs)
end
end
end
defp summarized(msgs) do
UI.report_step("[compaction]", "Summarizing conversation")
with {:ok, summarized_msgs} <- summarize(msgs) do
{:ok, summarized_msgs}
end
end
# ----------------------------------------------------------------------------
# Tersification: condenses messages individually. Only those messages up to
# (but not including) the most recent user message are compacted.
# ----------------------------------------------------------------------------
@tersify_model AI.Model.fast()
@tersify_prompt """
You are an AI agent that compacts messages by restating them as tersely as possible without losing any information.
The message below is part of a conversation between the user and an AI assistant.
Drop filler. Use short words. Abbrev where possible. Skip mb chars unless critical.
Keep just enough to preserve context. It's an LLM, so it will hallucinate to fill in the gaps.
If there are numbered items or bullets, keep them (the user may have referenced them by number later, and we don't want to break that), but shorten them as much as possible.
You must respond ONLY with the compacted message content, reduced in length as much as possible while preserving original meaning.
Do not include ANY additional commentary or explanation.
Your response most ONLY be the compacted message in the same voice as the original.
"""
defp tersify(msgs) do
# Split messages into two groups:
# 1. Messages to retain as-is: from the most recent user message onward
# 2. Messages to compact: all messages before the most recent user message
{to_compact, to_retain} =
msgs
|> Enum.reverse()
|> Enum.find_index(fn
%{role: "user"} -> true
_ -> false
end)
# Note that this step extracts from the original msgs, not the reversed one
|> then(fn
nil -> {[], msgs}
idx -> Enum.split(msgs, length(msgs) - idx - 1)
end)
UI.progress_bar_start(:tersifier, "Squeezing *really* hard", length(to_compact))
with {:ok, tersified} <- tersify_msgs(to_compact) do
{:ok, tersified ++ to_retain}
end
end
defp tersify_msgs(msgs) do
msgs
|> Util.async_stream(fn msg ->
result = tersify_msg(msg)
UI.progress_bar_update(:tersifier)
result
end)
|> Enum.reduce_while([], fn
{:ok, {:ok, msg}}, acc -> {:cont, [msg | acc]}
{:ok, {:error, reason}}, _acc -> {:halt, {:error, reason}}
{:exit, {:timeout, _}}, _acc -> {:halt, {:error, :timeout}}
{:exit, reason}, _acc -> {:halt, {:error, reason}}
{:throw, reason}, _acc -> {:halt, {:error, reason}}
end)
|> case do
{:error, reason} -> {:error, reason}
msgs -> {:ok, Enum.reverse(msgs)}
end
end
defp tersify_msg(%{content: nil} = msg), do: {:ok, msg}
defp tersify_msg(%{content: content} = msg) do
# Do not tersify previously generated summaries or already-tersified messages
if is_summary_msg?(msg) or is_tersified_msg?(msg) do
{:ok, msg}
else
is_thought? = has_think_tags?(msg)
AI.Completion.get(
model: @tersify_model,
replay_conversation: false,
compact?: false,
messages: [
AI.Util.system_msg(@tersify_prompt),
AI.Util.user_msg(content)
]
)
|> case do
{:ok, %{response: response}} ->
content = String.trim(response)
content =
if is_thought? do
"<think><fnord-meta:tersified />#{content}</think>"
else
"<fnord-meta:tersified />#{content}"
end
{:ok, %{msg | content: content}}
{:error, reason} ->
UI.warn("[compaction]", """
Error compacting message:
# Original Message:
#{inspect(msg, pretty: true, limit: :infinity)}
# Reason:
#{inspect(reason, pretty: true, limit: :infinity)}
""")
{:ok, msg}
end
end
end
# ----------------------------------------------------------------------------
# Summarization: takes the entire message transcript and summarizes it into a
# single message.
# ----------------------------------------------------------------------------
@summarize_model AI.Model.fast()
@summarize_prompt """
You are an AI agent to compacts a conversation between the user and an AI assistant into *proper* meeting minutes.
Your audience is the LLM itself, which will use these minutes to restore context for the next completion.
It is important to preserve context in a way that will allow the AI assistant to perform it's next completion accurately, without any break in continuity.
Build a compressed script of the conversation that captures the linear flow of ideas, instructions, and responses.
It is essential that the LLM's context is restored with the complete cascade of information, but in as few tokens as possible.
Retain as much detail as possible from user messages.
Condense tool call results as much as possible while preserving all facts discovered.
Compress assistant "thought" messages very aggressively.
Compress assistant responses to the user moderately, focusing on preserving key information and facts rather than social niceties.
A successful response can be read as-is to understand the complete conversation, with enough context for the LLM to recognize the evolution of the discussion.
Respond ONLY with the compact transcript, without any additional commentary or explanation.
"""
@summarizer_task """
Provide a highly compacted transcription of the conversation, ensuring all critical information is preserved for context restoration.
Consider how someone reading your summary would need to understand the flow of the conversation.
Be aware of flows that might cause the LLM to lose context or hallucinate, since they will ONLY have your summary as their entire context.
Do not omit anything that might harm the user's experience or the relevance of the LLM's next completion.
"""
defp summarize(msgs) do
UI.spin("Summarizing conversation", fn ->
transcript = AI.Util.research_transcript(msgs)
result =
AI.Accumulator.get_response(
compact?: false,
model: @summarize_model,
prompt: @summarize_prompt,
question: @summarizer_task,
input: transcript
)
|> case do
{:ok, %{response: response}} ->
{:ok,
[
AI.Util.user_msg("""
<fnord-meta:summary />
Summary of previous conversation:
#{response}
-----
We were having the discussion outlined above.
Please continue responding based on that context.
""")
]}
{:error, reason} ->
{:error, reason}
end
{"Conversation summarized", result}
end)
end
# ----------------------------------------------------------------------------
# Utilities
# ----------------------------------------------------------------------------
defp has_think_tags?(%{content: content}) when is_binary(content) do
content
|> String.trim()
|> String.downcase()
|> String.starts_with?("<think>")
end
defp has_think_tags?(_), do: false
defp is_summary_msg?(%{content: content}) when is_binary(content) do
content
|> String.trim()
|> String.starts_with?("<fnord-meta:summary />")
end
defp is_summary_msg?(_), do: false
defp is_tersified_msg?(%{content: content}) when is_binary(content) do
s = String.trim(content)
cond do
String.starts_with?(s, "<fnord-meta:tersified />") ->
true
String.downcase(s) |> String.starts_with?("<think>") ->
inner =
s
|> String.replace(~r/^<think>/i, "")
|> String.replace(~r/<\/think>$/i, "")
|> String.trim()
String.starts_with?(inner, "<fnord-meta:tersified />")
true ->
false
end
end
defp is_tersified_msg?(_), do: false
end