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:
@@ -6,7 +6,7 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.features.memory.enrichment import enrich_memory_after_ingest_sync
|
||||
from app.features.memory.enrichment import enrich_memory_after_ingest_async
|
||||
from app.features.memory.llm_schemas import EnrichmentPayload, parse_json_payload
|
||||
from app.features.memory.models import MemorySource
|
||||
from app.features.user.models import User
|
||||
@@ -26,7 +26,8 @@ def test_enrichment_payload_roundtrip() -> None:
|
||||
assert p.facts[0].subject == "王伟"
|
||||
|
||||
|
||||
def test_enrich_memory_after_ingest_sync_single_llm_call(
|
||||
@pytest.mark.asyncio
|
||||
async def test_enrich_memory_after_ingest_async_single_llm_call(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from app.features.memory import enrichment as mod
|
||||
@@ -35,43 +36,56 @@ def test_enrich_memory_after_ingest_sync_single_llm_call(
|
||||
|
||||
invoke_count = {"n": 0}
|
||||
|
||||
def fake_invoke(llm, prompt, max_tokens, agent):
|
||||
async def fake_invoke(llm, prompt, max_tokens, agent):
|
||||
invoke_count["n"] += 1
|
||||
assert agent == "memory.enrichment_sync"
|
||||
assert agent == "memory.enrichment"
|
||||
return (
|
||||
'{"summary":"本轮要点",'
|
||||
'"facts":[{"fact_type":"event","subject":"王伟","predicate":"住",'
|
||||
'"object_json":{"value":"上海"},"confidence":0.8,"source_chunk_id":"ch1"}]}'
|
||||
)
|
||||
|
||||
monkeypatch.setattr(mod, "invoke_json_object", fake_invoke)
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"list_chunks_for_source_sync",
|
||||
lambda s, sid: [SimpleNamespace(id="ch1", content="王伟住在上海。")],
|
||||
)
|
||||
monkeypatch.setattr(mod, "ainvoke_json_object", fake_invoke)
|
||||
|
||||
summaries: list[dict] = []
|
||||
facts: list[dict] = []
|
||||
|
||||
def capture_summary(session, **kwargs):
|
||||
async def capture_summary(session, **kwargs):
|
||||
summaries.append(kwargs)
|
||||
|
||||
def capture_fact(session, **kwargs):
|
||||
async def capture_fact(session, **kwargs):
|
||||
facts.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(mod, "create_memory_summary_sync", capture_summary)
|
||||
monkeypatch.setattr(mod, "create_memory_fact_sync", capture_fact)
|
||||
monkeypatch.setattr(mod, "create_memory_summary", capture_summary)
|
||||
monkeypatch.setattr(mod, "create_memory_fact", capture_fact)
|
||||
|
||||
class FakeResult:
|
||||
def unique(self):
|
||||
return self
|
||||
|
||||
def scalars(self):
|
||||
return self
|
||||
|
||||
def all(self):
|
||||
return [SimpleNamespace(id="ch1", content="王伟住在上海。")]
|
||||
|
||||
class FakeSession:
|
||||
def get(self, model, key):
|
||||
async def get(self, model, key):
|
||||
if model is User and key == "u1":
|
||||
return SimpleNamespace(nickname="老王")
|
||||
if model is MemorySource and key == "src-1":
|
||||
return SimpleNamespace(lineage_json=None)
|
||||
return None
|
||||
|
||||
enrich_memory_after_ingest_sync(FakeSession(), "u1", "src-1", llm=object())
|
||||
async def execute(self, _stmt):
|
||||
return FakeResult()
|
||||
|
||||
await enrich_memory_after_ingest_async(
|
||||
FakeSession(), # type: ignore[arg-type]
|
||||
"u1",
|
||||
"src-1",
|
||||
llm=object(),
|
||||
)
|
||||
|
||||
assert invoke_count["n"] == 1
|
||||
assert len(summaries) == 1
|
||||
@@ -83,38 +97,54 @@ def test_enrich_memory_after_ingest_sync_single_llm_call(
|
||||
assert facts[0]["status"] == "confirmed"
|
||||
|
||||
|
||||
def test_enrich_memory_skips_when_parse_returns_none(
|
||||
@pytest.mark.asyncio
|
||||
async def test_enrich_memory_skips_when_parse_returns_none(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from app.features.memory import enrichment as mod
|
||||
|
||||
monkeypatch.setattr("app.core.config.settings.memory_enrichment_enabled", True)
|
||||
monkeypatch.setattr(mod, "invoke_json_object", lambda *a, **k: "{not json")
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"list_chunks_for_source_sync",
|
||||
lambda s, sid: [SimpleNamespace(id="c1", content="x")],
|
||||
)
|
||||
|
||||
async def fake_invoke(*_args, **_kwargs):
|
||||
return "{not json"
|
||||
|
||||
monkeypatch.setattr(mod, "ainvoke_json_object", fake_invoke)
|
||||
called = {"summary": False, "fact": False}
|
||||
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"create_memory_summary_sync",
|
||||
lambda *a, **k: called.update(summary=True),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"create_memory_fact_sync",
|
||||
lambda *a, **k: called.update(fact=True),
|
||||
)
|
||||
async def capture_summary(*_args, **_kwargs):
|
||||
called.update(summary=True)
|
||||
|
||||
async def capture_fact(*_args, **_kwargs):
|
||||
called.update(fact=True)
|
||||
|
||||
monkeypatch.setattr(mod, "create_memory_summary", capture_summary)
|
||||
monkeypatch.setattr(mod, "create_memory_fact", capture_fact)
|
||||
|
||||
class FakeResult:
|
||||
def unique(self):
|
||||
return self
|
||||
|
||||
def scalars(self):
|
||||
return self
|
||||
|
||||
def all(self):
|
||||
return [SimpleNamespace(id="c1", content="x")]
|
||||
|
||||
class FakeSession:
|
||||
def get(self, model, key):
|
||||
async def get(self, model, key):
|
||||
if model is User and key == "u":
|
||||
return None
|
||||
if model is MemorySource and key == "s":
|
||||
return SimpleNamespace(lineage_json=None)
|
||||
return None
|
||||
|
||||
enrich_memory_after_ingest_sync(FakeSession(), "u", "s", llm=object())
|
||||
async def execute(self, _stmt):
|
||||
return FakeResult()
|
||||
|
||||
await enrich_memory_after_ingest_async(
|
||||
FakeSession(), # type: ignore[arg-type]
|
||||
"u",
|
||||
"s",
|
||||
llm=object(),
|
||||
)
|
||||
assert called == {"summary": False, "fact": False}
|
||||
|
||||
Reference in New Issue
Block a user