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

@@ -36,7 +36,6 @@ from app.agents.stage_constants import (
)
from app.agents.state_schema import MemoirStateSchema
from app.core.config import settings
from app.core.dependencies import get_embedding_provider
from app.core.logging import get_logger
from app.features.conversation.lineage_schemas import aggregate_lineage_from_segments
from app.features.memoir.chapter_evidence_snapshot import (
@@ -60,7 +59,6 @@ from app.features.memoir.repo import (
mark_chapter_dirty_sync,
reorder_chapter_story_links_by_life_order_sync,
)
from app.features.memory.repo import retrieve_evidence_sync
from app.features.story.models import Story, StoryVersion
from app.features.story.sync_write import (
append_story_version_sync,
@@ -102,7 +100,7 @@ def _dialogue_lineage_dict_for_segment_ids(
def _evidence_link_ids(
evidence: dict,
) -> tuple[list[str], list[str], list[str], list[str]]:
"""retrieve_evidence_sync 结果提取稳定 ID 列表。"""
"""MemoryService.retrieve 结果提取稳定 ID 列表。"""
chunks: list[str] = []
for c in evidence.get("relevant_chunks") or []:
if isinstance(c, dict) and c.get("id"):
@@ -960,6 +958,7 @@ def run_story_pipeline_for_category_batch(
occupation: str = "",
memoir_correlation_id: str | None = None,
llm_fast: Any | None = None,
memory_evidence: dict | None = None,
) -> tuple[Chapter | None, bool, set[str]]:
"""
返回 (chapter, needs_cover_enqueue, story_ids_to_dispatch_after_commit)。
@@ -979,8 +978,6 @@ def run_story_pipeline_for_category_batch(
top_k = int(settings.evidence_top_k_default)
if n_units > int(settings.evidence_large_batch_threshold):
top_k = int(settings.evidence_top_k_large_batch)
emb = get_embedding_provider()
embedding_available = emb.is_available()
def _oral_job() -> tuple[str, float]:
t_oral = time.perf_counter()
@@ -991,23 +988,13 @@ def run_story_pipeline_for_category_batch(
with ThreadPoolExecutor(max_workers=1) as pool:
oral_future = pool.submit(_oral_job)
_t_ev = time.perf_counter()
try:
evidence = retrieve_evidence_sync(
session,
user_id,
combined_text,
top_k=top_k,
embedding_provider=emb,
)
except Exception as e:
logger.warning("Evidence 检索跳过: {}", e)
evidence = {
"relevant_chunks": [],
"relevant_summaries": [],
"relevant_facts": [],
"timeline_hints": [],
"relevant_stories": [],
}
evidence = memory_evidence or {
"relevant_chunks": [],
"relevant_summaries": [],
"relevant_facts": [],
"timeline_hints": [],
"relevant_stories": [],
}
ev_elapsed = time.perf_counter() - _t_ev
oral_for_memoir, oral_elapsed = oral_future.result()
pipeline_phase_timings["evidence"] = ev_elapsed
@@ -1017,13 +1004,13 @@ def run_story_pipeline_for_category_batch(
)
logger.info(
"memoir_evidence_retrieved user_id={} chunks={} facts={} summaries={} stories={} vector_ok={}",
"memoir_evidence_retrieved user_id={} chunks={} facts={} summaries={} stories={} top_k={}",
user_id,
len(evidence.get("relevant_chunks") or []),
len(evidence.get("relevant_facts") or []),
len(evidence.get("relevant_summaries") or []),
len(evidence.get("relevant_stories") or []),
embedding_available,
top_k,
)
evidence_text = format_evidence_chunks_for_prompt(evidence)