Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions core/loop/memory_retrieval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""P1-6 (GenAI lesson 15): explicit mitigation for retrieval failure modes.

Lesson 15 names three failure modes an agent memory loop must handle
explicitly instead of best-effort guessing:

1. **Retrieved nothing** — no similar entry in the store. Mitigation: a
similarity threshold plus an explicit "no memory" fallback (never
best-effort answering from vague similarity).
2. **Retrieved the wrong thing** — pure semantic recall misses identifiers /
API names. Mitigation: hybrid keyword + vector recall (cerebellum's
``memory_search`` already does this); the DeepCode side enforces the
threshold on whatever the store returned.
3. **Retrieved but not used** — the course's own notebook retrieved chunks
into ``history`` yet only injected ``history[-1]``: retrieved data never
reached the prompt. Mitigation: :func:`assert_all_injected` — a self-check
that every accepted entry actually appears in the injected text.

Pure mechanism, no LLM. Works with any store that returns scored entries
``{"content", "similarity", ...}`` (cerebellum ``semantic_hits`` shape).
"""

from __future__ import annotations

from collections.abc import Iterable
from typing import Any

from core.loop.injection_regression import render_data_block

# Below this similarity the entry is not "relevant enough" to inject — the
# caller should fall back to the explicit no-memory statement. Cerebellum
# already hard-filters at 0.45 internally; this is the DeepCode-side
# contract applied to whatever the store returned (default aligns).
DEFAULT_SIMILARITY_THRESHOLD = 0.45

_NO_MEMORY_STATEMENT = (
"No relevant past-session memory was found for this topic. Proceed from "
"first principles; do not invent facts attributed to past sessions."
)


def accepted_entries(
entries: Iterable[dict[str, Any]],
threshold: float = DEFAULT_SIMILARITY_THRESHOLD,
) -> list[dict[str, Any]]:
"""Entries whose similarity is at/above ``threshold``, sorted by score.

Accepts both cerebellum ``semantic_hits`` rows (``similarity`` key) and
generic ``{"content", "score"}`` shapes (``score`` aliases similarity).
"""
accepted: list[dict[str, Any]] = []
for entry in entries or []:
if not isinstance(entry, dict):
continue
similarity = entry.get("similarity")
if similarity is None:
similarity = entry.get("score")
try:
value = float(similarity)
except (TypeError, ValueError):
continue
if value >= threshold and str(entry.get("content", "")).strip():
accepted.append(entry)
return sorted(accepted, key=lambda e: float(e.get("similarity") or 0), reverse=True)


def compose_memory_injection(
entries: Iterable[dict[str, Any]],
threshold: float = DEFAULT_SIMILARITY_THRESHOLD,
) -> str:
"""Render accepted entries as a numbered, data-bounded injection block.

Each entry becomes one ``<untrusted-data>`` block carrying the P1-3
restrict clause (reference only, never instructions) plus traceable
metadata — source key, source layer, and creation timestamp when present
(P2-D3, lessons 08/14: retrieved results carry locators so answers can be
grounded and attributed). Entries are numbered ``[n]``; the model may cite
``[n]`` to attribute a claim to a specific memory.
Empty when nothing clears the threshold — the caller then uses
:func:`no_memory_statement` instead of injecting weak matches.
"""
accepted = accepted_entries(entries, threshold=threshold)
blocks: list[str] = []
for index, entry in enumerate(accepted, start=1):
content = str(entry.get("content", "")).strip()
if not content:
continue
source = entry.get("source_key") or entry.get("source") or "memory"
created_at = entry.get("created_at") or entry.get("timestamp")
header = f"[{index}] (from {source}"
if created_at:
header += f", at {created_at}"
header += ")"
blocks.append(f"{header}\n{render_data_block(content)}")
if not blocks:
return ""
# P3-C (GenAI lessons 08/15): explicit citation guidance. The numbered
# [n] labels are only useful if the model is told it may cite them —
# without this line it may restate a memory verbatim without attribution,
# losing the grounding the labels exist to provide.
blocks.append(
"When you rely on any numbered memory above, cite it as [n] so the "
"answer stays attributable to its source."
)
return "\n\n".join(blocks)


def no_memory_statement() -> str:
"""The explicit fallback when retrieval cleared nothing (failure mode 1)."""
return _NO_MEMORY_STATEMENT


def assert_all_injected(entries: Iterable[dict[str, Any]], injected: str) -> list[str]:
"""Failure-mode-3 self-check: every *accepted* entry must appear verbatim
in the injected text.

Returns the list of accepted entries whose content is missing from
``injected`` (empty = the injection chain is intact). A non-empty result
means the retrieval layer found data the prompt layer dropped — the exact
"retrieved but not used" bug from the course notebook.
"""
missing: list[str] = []
for entry in accepted_entries(entries):
content = str(entry.get("content", "")).strip()
if content and content not in injected:
missing.append(content[:80])
return missing


__all__ = [
"DEFAULT_SIMILARITY_THRESHOLD",
"accepted_entries",
"assert_all_injected",
"compose_memory_injection",
"no_memory_statement",
]
Loading