Packages
fnord
0.8.79
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/associative_learning.ex
defmodule AI.Agent.Memory.AssociativeLearning do
@moduledoc """
Agent that scores memories for a conversation.
Given a conversation and a list of `AI.Memory` structs, it asks the LLM to
assign each memory a relevance score from 1–10 and returns a map of
`memory_id => score` keyed by each memory's `id`.
Response format:
{:ok, conversation_id, %{"memory_id_1" => score_1, ...}}
On transient decode/validation failures, the agent will retry up to
`@retry_limit` times before returning an error.
"""
@type conversation_id :: String.t()
@type memory_score_map :: %{optional(String.t()) => pos_integer()}
@retry_limit 3
@associative_high_threshold 9
@associative_low_threshold 2
@associative_strengthen_delta 0.2
@associative_weaken_delta -0.2
# ----------------------------------------------------------------------------
# Behaviour implementation
# ----------------------------------------------------------------------------
@behaviour AI.Agent
@model AI.Model.large_context(:balanced)
@system_prompt """
You are an associative memory selection and scoring helper inside a larger
agent system.
Your job is to read the current conversation and a list of candidate
memories, then assign each memory a *relevance score* from 1 (barely
relevant) to 10 (extremely central) **for this specific conversation**.
Important rules:
- You MUST return a JSON object where each key is a memory id (string) and
each value is an integer score from 1 to 10.
- Every provided memory id MUST appear in the object, even if you think it
is only weakly relevant.
- Use the full range 1–10 when appropriate; do not cluster all scores at one
value.
- Base your judgment only on the provided conversation messages and memory
descriptions.
- If a memory seems unrelated to the conversation, give it a low score like
1 or 2, but still include it in the result.
"""
@response_format %{
type: "json_schema",
json_schema: %{
name: "memory_relevance_scores",
strict: true,
schema: %{
type: "object",
description: """
Map of memory ids to relevance scores (integer 1–10) for the current
conversation.
""",
required: [],
additionalProperties: %{
type: "integer",
minimum: 1,
maximum: 10
},
properties: %{}
}
}
}
@doc """
Entry point required by the `AI.Agent` behaviour.
Expected args map:
%{
agent: %AI.Agent{},
conversation: %Store.Project.Conversation{} | %{id: id, messages: messages},
memories: [%AI.Memory{}, ...]
}
Returns `{:ok, scores}` on success, where `scores` is a map of memory IDs (as
strings) to integer scores in the range 1–10.
"""
@impl AI.Agent
@spec get_response(map()) :: {:ok, memory_score_map} | {:error, term()}
def get_response(%{agent: agent, conversation: conversation, memories: memories}) do
with {:ok, scores} <- do_score_with_retries(agent, conversation, memories, @retry_limit) do
{:ok, scores}
end
end
def get_response(_), do: {:error, :invalid_arguments}
# ----------------------------------------------------------------------------
# Core scoring flow with simple retry
# ----------------------------------------------------------------------------
defp do_score_with_retries(_agent, _conversation, _memories, 0) do
{:error, :max_retries_exceeded}
end
defp do_score_with_retries(agent, conversation, memories, attempts_left) do
case score_once(agent, conversation, memories) do
{:ok, scores} ->
{:ok, scores}
{:error, _reason} ->
do_score_with_retries(agent, conversation, memories, attempts_left - 1)
end
end
@spec score_once(AI.Agent.t(), any, [AI.Memory.t()]) ::
{:ok, memory_score_map}
| {:error, term()}
defp score_once(agent, conversation, memories) do
messages = build_messages(conversation, memories)
case AI.Agent.get_completion(agent,
model: @model,
messages: messages,
response_format: @response_format
) do
{:ok, %AI.Completion{response: response}} ->
response
|> decode_scores()
|> apply_scores(conversation, memories)
{:error, reason} ->
{:error, reason}
end
end
# ----------------------------------------------------------------------------
# Prompt construction
# ----------------------------------------------------------------------------
defp build_messages(conversation, memories) do
convo_block = format_conversation(conversation)
memories_block = format_memories(memories)
user_content = """
Here is the current conversation:
#{convo_block}
-----
Here are the candidate memories:
#{memories_block}
Return ONLY a JSON object mapping memory ids (as strings) to integer
relevance scores from 1 to 10.
"""
[
AI.Util.system_msg(@system_prompt),
AI.Util.user_msg(user_content)
]
end
defp format_conversation(%Store.Project.Conversation{} = convo) do
{:ok, _ts, messages, _metadata} = Store.Project.Conversation.read(convo)
format_messages(messages)
end
defp format_conversation(%{messages: messages}) when is_list(messages) do
format_messages(messages)
end
defp format_conversation(_), do: "(no conversation messages available)"
defp format_messages(messages) do
messages
|> Enum.with_index(1)
|> Enum.map(fn {msg, idx} ->
role = Map.get(msg, :role) || Map.get(msg, "role") || "unknown"
content = Map.get(msg, :content) || Map.get(msg, "content") || ""
"[#{idx}] #{role}: #{content}"
end)
|> Enum.join("\n")
end
defp format_memories(memories) do
memories
|> Enum.map(fn %AI.Memory{
id: id,
label: label,
scope: scope,
response_template: template
} = mem ->
scope_str = to_string(scope)
# Fallbacks in case some fields are nil
label = label || id || "(no label)"
template = template || "(no response template)"
pattern_info =
case Map.get(mem, :pattern_tokens) do
%{} = tokens when map_size(tokens) > 0 ->
tokens
|> Enum.take(5)
|> Enum.map_join(", ", fn {tok, weight} -> "#{tok}:#{weight}" end)
|> then(&"pattern_tokens: #{&1}")
_ ->
"pattern_tokens: (none)"
end
"""
- id: #{id}
scope: #{scope_str}
label: #{label}
template: #{template}
#{pattern_info}
"""
end)
|> Enum.join("\n\n")
end
# ----------------------------------------------------------------------------
# Response handling and validation
# ----------------------------------------------------------------------------
@spec decode_scores(String.t()) :: {:ok, memory_score_map} | {:error, term()}
defp decode_scores(json) when is_binary(json) do
case Jason.decode(json) do
{:ok, %{} = scores} -> validate_scores(scores)
{:ok, _other} -> {:error, :invalid_response_shape}
{:error, reason} -> {:error, {:decode_error, reason}}
end
end
defp decode_scores(_), do: {:error, :invalid_response}
@spec validate_scores(map()) :: {:ok, memory_score_map} | {:error, term()}
defp validate_scores(scores) when is_map(scores) do
with true <-
Enum.all?(scores, fn {k, v} ->
is_binary(k) &&
is_integer(v) &&
v >= 1 &&
v <= 10
end) do
{:ok, scores}
else
_ -> {:error, :invalid_scores}
end
end
defp apply_scores({:error, reason}, _, _), do: {:error, reason}
defp apply_scores({:ok, scores}, match_input, memories) do
memories
|> Enum.each(fn memory ->
scores
|> Map.get(memory.id)
|> case do
score when is_integer(score) and score >= @associative_high_threshold ->
UI.debug("Relearning", "(#{memory.scope}) #{memory.label}")
updated = AI.Memory.train(memory, match_input, @associative_strengthen_delta)
Services.Memories.update(updated)
score when is_integer(score) and score <= @associative_low_threshold ->
UI.debug("Unlearning", "(#{memory.scope}) #{memory.label}")
updated = AI.Memory.train(memory, match_input, @associative_weaken_delta)
Services.Memories.update(updated)
_ ->
nil
end
end)
{:ok, scores}
end
end