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

@@ -31,7 +31,11 @@ from app.agents.chat.reply_limits import (
)
from app.agents.chat.reply_planner import maybe_refine_turn_plan_with_llm
from app.agents.chat.stage_detection import keyword_fallback_primary_stage
from app.agents.state_schema import MemoirStateSchema
from app.agents.state_schema import (
MemoirStateSchema,
interview_control_state,
narrative_coverage_state,
)
from app.core.agent_logging import (
agent_span,
log_agent_payload,
@@ -154,14 +158,14 @@ class InterviewAgent:
text_for_model = self._resolve_text_for_model(
user_message, normalized_user_message
)
empty_slots = memoir_state.prompt_empty_slots_for_current_stage()
filled_slots = {
key: value.snippet
for key, value in memoir_state.slots.get(
memoir_state.current_stage, {}
).items()
if value.snippet
}
narrative_state = narrative_coverage_state(memoir_state)
control_state = interview_control_state(memoir_state)
empty_slots = control_state.prompt_empty_slots_for_stage(
narrative_state, memoir_state.current_stage
)
filled_slots = narrative_state.filled_slots_for_stage(
memoir_state.current_stage
)
if detected_user_stage is not None:
du = detected_user_stage
else:
@@ -173,7 +177,7 @@ class InterviewAgent:
)
recent_questions = extract_recent_questions(hw.window)
conversation_turn_total = hw.turn_total
all_stages_coverage = memoir_state.all_stages_coverage()
all_stages_coverage = narrative_state.all_stages_coverage()
persona = normalize_interview_persona(settings.chat_interview_persona)
max_segments = int(settings.chat_interview_max_segments)
max_tokens = int(settings.chat_interview_max_tokens)
@@ -406,7 +410,11 @@ class InterviewAgent:
if not self.llm:
return ["你好呀~ 又见面了。今天想从人生里哪一小段回忆开始聊聊?"]
try:
empty_slots = memoir_state.prompt_empty_slots_for_current_stage()
narrative_state = narrative_coverage_state(memoir_state)
control_state = interview_control_state(memoir_state)
empty_slots = control_state.prompt_empty_slots_for_stage(
narrative_state, memoir_state.current_stage
)
empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots]
persona = normalize_interview_persona(settings.chat_interview_persona)
prompt = get_opening_prompt(