Packages
fnord
0.9.36
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/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