refactor(memory): extract F2 recall path (#23)
This commit is contained in:
@@ -361,34 +361,21 @@ def search_cmd(
|
||||
from rich.table import Table
|
||||
|
||||
from tht.cli.vector_cmd import make_embedder, open_searcher, require_vector_cfg
|
||||
from tht.memory import REUSABLE_TYPES, decided_memory_ids, load_registry
|
||||
from tht.memory import load_registry, recall_memories
|
||||
|
||||
cfg = _load_config_or_exit(config)
|
||||
require_vector_cfg(cfg)
|
||||
excluded: set[str] = set()
|
||||
if session is not None:
|
||||
excluded = decided_memory_ids(load_snapshot_or_exit(cfg, session).decisions)
|
||||
decisions = load_snapshot_or_exit(cfg, session).decisions if session is not None else []
|
||||
searcher = open_searcher(cfg)
|
||||
embedder = make_embedder(cfg.embeddings)
|
||||
hits = searcher.search(embedder.embed_query(question), top_n=top, kinds=["memory"])
|
||||
by_id = {r.id: r for r in load_registry(registry_path(cfg))}
|
||||
|
||||
results = []
|
||||
for h in hits:
|
||||
rec = by_id.get(h.ref)
|
||||
if rec is None:
|
||||
continue # indice piu' avanti del registro: ignora
|
||||
if rec.type not in REUSABLE_TYPES:
|
||||
continue # record legacy non-concept: non e' una memory riusabile
|
||||
if rec.id in excluded:
|
||||
continue # gia' decisa in questa sessione: non riproporla
|
||||
results.append({
|
||||
"id": rec.id, "type": rec.type, "subject": rec.subject,
|
||||
"detail": rec.detail, "rationale": rec.rationale,
|
||||
"question_context": rec.question_context,
|
||||
"tables": rec.tables, "concepts": rec.concepts,
|
||||
"session_id": rec.session_id, "score": round(h.similarity, 4),
|
||||
})
|
||||
results = recall_memories(
|
||||
question,
|
||||
records=load_registry(registry_path(cfg)),
|
||||
decisions=decisions,
|
||||
searcher=searcher,
|
||||
embedder=embedder,
|
||||
top=top,
|
||||
)
|
||||
|
||||
if json_out:
|
||||
typer.echo(json.dumps(results, ensure_ascii=False, indent=2))
|
||||
|
||||
@@ -137,6 +137,48 @@ REUSABLE_TYPES = frozenset({"concept_clarified"})
|
||||
MAX_PROMOTION_CANDIDATES = 5
|
||||
|
||||
|
||||
def recall_memories(
|
||||
question: str,
|
||||
*,
|
||||
records: list[MemoryRecord],
|
||||
decisions: list[DecisionRecord],
|
||||
searcher,
|
||||
embedder,
|
||||
top: int = 5,
|
||||
) -> list[dict]:
|
||||
"""Return reviewer-visible F2 candidates in semantic-search rank order.
|
||||
|
||||
Memory owns filtering against the canonical registry, reusable decision types,
|
||||
and decisions already persisted in the current (including resumed) session.
|
||||
The CLI remains a thin adapter that supplies the vector ports and renders output.
|
||||
"""
|
||||
excluded = decided_memory_ids(decisions)
|
||||
hits = searcher.search(
|
||||
embedder.embed_query(question),
|
||||
top_n=top,
|
||||
kinds=["memory"],
|
||||
)
|
||||
by_id = {record.id: record for record in records}
|
||||
results = []
|
||||
for hit in hits:
|
||||
record = by_id.get(hit.ref)
|
||||
if record is None or record.type not in REUSABLE_TYPES or record.id in excluded:
|
||||
continue
|
||||
results.append({
|
||||
"id": record.id,
|
||||
"type": record.type,
|
||||
"subject": record.subject,
|
||||
"detail": record.detail,
|
||||
"rationale": record.rationale,
|
||||
"question_context": record.question_context,
|
||||
"tables": record.tables,
|
||||
"concepts": record.concepts,
|
||||
"session_id": record.session_id,
|
||||
"score": round(hit.similarity, 4),
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def _compute_promotions(
|
||||
session_dir: Path, manifest: SessionManifest, *,
|
||||
seqs: list[int] | None, existing: list[MemoryRecord],
|
||||
|
||||
Reference in New Issue
Block a user