feat(api)!: memory single chain — async MemoryService, strict eval closure

Route all memory ingest/retrieve/enrichment/compaction through async MemoryService.
Remove legacy sync memory implementations (ingest/retrieve/compaction); Celery and
memoir Phase2 call asyncio.run into MemoryService-backed helpers.

Memoir Phase1 batch ingest uses MemoryService.ingest_transcripts_batch; drop chapters.
evidence_bundle_json mirror (Alembic 0015). Evaluation uses snapshot/link-only bundles;
raise EvidenceClosureMissing instead of partial/fallback lineage tiers.

Split memoir state into NarrativeCoverageState and InterviewControlState; delete the
_interview_meta_store adapter layer. Remove rolling-query and recent-fact fallback
settings from config and evidence assembly.

Update judges, docs, tests, and PlaygroundPage alignment.

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-30 14:11:46 +08:00
parent ac436b87a2
commit 71fbd39e32
53 changed files with 953 additions and 2448 deletions

View File

@@ -2,73 +2,18 @@
import pytest
from app.core.config import settings
from app.features.memory import evidence as evidence_mod
from app.features.memory.evidence_format import format_evidence_chunks_for_chat_prompt
from app.features.memory.evidence import (
EMPTY_EVIDENCE_BUNDLE,
_facts_to_dicts,
_stories_to_dicts,
_timeline_to_dicts,
retrieve_evidence_bundle_async,
retrieve_evidence_bundle_sync,
)
from app.features.memory.evidence_format import format_evidence_chunks_for_chat_prompt
from app.features.memory.schemas import EvidenceBundle
class _FakeEmbedding:
def is_available(self) -> bool:
return True
def embed_text_sync(self, text: str) -> list[float]:
return [0.25, 0.5, 0.75]
def test_retrieve_evidence_bundle_sync_uses_vector_search(
monkeypatch: pytest.MonkeyPatch,
) -> None:
searched: list[tuple] = []
def fake_search(session, user_id, emb, top_k):
searched.append((user_id, emb, top_k))
return [
{
"id": "c1",
"content": "chunk body",
"chunk_index": 0,
"distance": 0.1,
}
]
def fake_meta(user_id, q, top_k):
return {
"relevant_facts": [],
"timeline_hints": [],
"relevant_summaries": [],
"relevant_stories": [],
}
monkeypatch.setattr(evidence_mod, "search_chunks_vector_sync", fake_search)
monkeypatch.setattr(
evidence_mod, "fetch_evidence_metadata_parallel_sync", fake_meta
)
out = retrieve_evidence_bundle_sync(
session=object(),
user_id="u1",
query=" hello ",
top_k=7,
embedding_provider=_FakeEmbedding(),
)
assert len(searched) == 1
assert searched[0][0] == "u1"
assert searched[0][1] == [0.25, 0.5, 0.75]
assert searched[0][2] == 7
assert out["relevant_chunks"] == [
{"id": "c1", "content": "chunk body", "chunk_index": 0},
]
def test_empty_evidence_bundle_keys() -> None:
assert set(EMPTY_EVIDENCE_BUNDLE.keys()) == {
"relevant_chunks",
@@ -238,10 +183,7 @@ async def test_retrieve_evidence_bundle_async_non_empty_merges_precomputed_chunk
assert out == {"relevant_chunks": merged, **meta}
async def test_empty_query_evidence_bundle_async_and_sync_aligned_when_rolling_off(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(settings, "memory_evidence_empty_query_include_rolling", False)
async def test_empty_query_evidence_bundle_async_returns_empty() -> None:
out_a = await retrieve_evidence_bundle_async(
object(),
"u1",
@@ -250,11 +192,3 @@ async def test_empty_query_evidence_bundle_async_and_sync_aligned_when_rolling_o
merged_chunk_dicts=[],
)
assert out_a == dict(EMPTY_EVIDENCE_BUNDLE)
out_s = retrieve_evidence_bundle_sync(
session=object(),
user_id="u1",
query="",
top_k=10,
embedding_provider=None,
)
assert out_s == dict(EMPTY_EVIDENCE_BUNDLE)