Packages

fnord

0.9.36

AI code archaeology

Current section

Files

Jump to
fnord lib ai agent memory deduplicator.ex
Raw

lib/ai/agent/memory/deduplicator.ex

defmodule AI.Agent.Memory.Deduplicator do
@moduledoc """
Agent that examines two same-scope long-term memories and decides whether to
merge them into a single synthesized memory.
Returns a structured JSON response (no prose) with shape:
{"merge": false}
or
{"merge": true, "title": "...", "content": "...", "topics": [...]}
The caller is responsible for saving the synthesized memory and deleting both
originals on a merge decision.
"""
@behaviour AI.Agent
# Dedup is a binary classification (merge yes/no) with an occasional small
# JSON synthesis on the merge path. Inputs are typically <2k tokens. A
# fast-tier model is plenty for the task and sits on a much higher TPM
# quota than the flagship tier - dedup runs in a burst at shutdown and
# would otherwise saturate the gpt-4.1 TPM bucket.
@model AI.Model.fast()
@prompt """
You are speaking to another LLM, not a human. Save tokens: use extremely terse, shorthand speech as long as meaning is clear.
You will be given two long-term memories (memory_a and memory_b). Decide
whether they should be merged into a single memory.
Return ONLY valid JSON -- no prose, no commentary:
- {"merge": false}
if the memories contain distinct information that should remain separate.
- {"merge": true, "title": "...", "content": "...", "topics": [...]}
if the memories substantially overlap or are redundant. Write a clean
synthesis that preserves all unique information from both. Choose a title
that accurately describes the combined content. Topics must be a flat array
of lowercase strings.
Be conservative. Only merge when the overlap is substantial and unambiguous.
When in doubt, return {"merge": false}.
Do NOT merge memories that merely share a topic but contain distinct
information. Do NOT discard information to make a merge fit. If merging
would require omitting something from either memory, do not merge.
"""
@impl AI.Agent
def get_response(opts) do
with {:ok, agent} <- Map.fetch(opts, :agent),
{:ok, payload} <- Map.fetch(opts, :payload) do
messages = [AI.Util.system_msg(@prompt), AI.Util.system_msg(payload)]
agent
|> AI.Agent.get_completion(
model: @model,
log_msgs: false,
log_tool_calls: false,
messages: messages,
toolbox: %{}
)
|> case do
{:ok, %{response: response}} -> {:ok, response}
{:error, reason} -> {:error, reason}
end
end
end
@doc """
Evaluates two same-scope memories and returns a merge decision.
Returns `{:ok, %{"merge" => false}}` to keep both, or
`{:ok, %{"merge" => true, "title" => ..., "content" => ..., "topics" => [...]}}`.
"""
@spec run(Memory.t(), Memory.t()) :: {:ok, map()} | {:error, term()}
def run(%Memory{} = a, %Memory{} = b) do
payload =
SafeJson.encode!(%{
memory_a: %{title: a.title, content: a.content, topics: a.topics},
memory_b: %{title: b.title, content: b.content, topics: b.topics}
})
with {:ok, response} <- invoke_agent(payload),
{:ok, decoded} <- SafeJson.decode(response),
:ok <- validate_response(decoded) do
{:ok, decoded}
end
end
defp invoke_agent(payload) do
__MODULE__
|> AI.Agent.new(named?: false)
|> AI.Agent.get_response(%{payload: payload})
end
defp validate_response(%{"merge" => false}), do: :ok
defp validate_response(%{
"merge" => true,
"title" => title,
"content" => content,
"topics" => topics
})
when is_binary(title) and title != "" and is_binary(content) and content != "" and
is_list(topics),
do: :ok
defp validate_response(_), do: {:error, :invalid_response}
end