WIP: memory system improvements (in progress)

Interview/chat prompt layers, reply planner, style profiles, memory
injection, interview meta store, and related tests. Work not finished.

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-22 16:56:28 +08:00
parent e848f26354
commit 3121d1384d
28 changed files with 2790 additions and 452 deletions

View File

@@ -4,7 +4,7 @@
"""
import uuid
from typing import Dict, List, cast
from typing import Dict, List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -15,6 +15,7 @@ from app.agents.stage_constants import (
normalize_chat_stage,
)
from app.agents.state_schema import (
InterviewControlState,
KnownFact,
MemoirStateSchema,
PersonaThread,
@@ -22,10 +23,9 @@ from app.agents.state_schema import (
default_state,
)
from app.core.config import settings
from app.features.memoir import _interview_meta_store as interview_meta
from app.features.memoir.models import MemoirState as MemoirStateModel
_INTERVIEW_STATE_META_KEY = "__interview_state__"
def _slots_snapshot_for_merge(raw: Dict[str, Dict] | None) -> Dict[str, Dict]:
"""浅拷贝 slots避免就地改 JSON 列同一 dict 引用导致 ORM 不标记 dirty。"""
@@ -34,67 +34,20 @@ def _slots_snapshot_for_merge(raw: Dict[str, Dict] | None) -> Dict[str, Dict]:
return {k: dict(v or {}) for k, v in raw.items()}
def _extract_interview_state_meta(
raw_slots: Dict[str, Dict] | None,
) -> tuple[list[KnownFact], list[PersonaThread], list[str]]:
if not raw_slots or not isinstance(raw_slots, dict):
return [], [], []
meta = raw_slots.get(_INTERVIEW_STATE_META_KEY)
if not isinstance(meta, dict):
return [], [], []
known = meta.get("known_facts") if isinstance(meta.get("known_facts"), list) else []
persona = (
meta.get("persona_threads")
if isinstance(meta.get("persona_threads"), list)
else []
)
recent = (
meta.get("recent_questions")
if isinstance(meta.get("recent_questions"), list)
else []
)
return (
[KnownFact.model_validate(x) for x in known if isinstance(x, dict)],
[PersonaThread.model_validate(x) for x in persona if isinstance(x, dict)],
[str(x).strip() for x in recent if str(x).strip()],
)
def _inject_interview_state_meta(
*,
slots: Dict[str, Dict],
known_facts: list[KnownFact],
persona_threads: list[PersonaThread],
recent_questions: list[str],
) -> Dict[str, Dict]:
out = dict(slots)
out[_INTERVIEW_STATE_META_KEY] = cast(
Dict,
{
"known_facts": [x.model_dump() for x in known_facts],
"persona_threads": [x.model_dump() for x in persona_threads],
"recent_questions": list(recent_questions),
},
)
return out
def coerce_memoir_state(model: MemoirStateModel) -> MemoirStateSchema:
"""把 ORM 行投影成 MemoirStateSchema控制元数据的读法已隔离在 interview_meta 适配层。"""
raw_slots = model.slots if isinstance(model.slots, dict) else None
known_facts, persona_threads, recent_questions = _extract_interview_state_meta(
raw_slots
)
clean_slots = dict(raw_slots) if raw_slots else dict(default_state().slots)
clean_slots.pop(_INTERVIEW_STATE_META_KEY, None)
control = interview_meta.read(raw_slots)
clean_slots = interview_meta.strip(raw_slots) or dict(default_state().slots)
return MemoirStateSchema.model_validate(
{
"stage_order": model.stage_order or default_state().stage_order,
"current_stage": model.current_stage,
"covered_stages": model.covered_stages or [],
"slots": clean_slots,
"known_facts": known_facts,
"persona_threads": persona_threads,
"recent_questions": recent_questions,
"known_facts": control.known_facts,
"persona_threads": control.persona_threads,
"recent_questions": control.recent_questions,
}
)
@@ -270,11 +223,13 @@ async def save_interview_state_meta(
slots = _slots_snapshot_for_merge(
state.slots if isinstance(state.slots, dict) else None
)
state.slots = _inject_interview_state_meta(
slots=slots,
known_facts=known_facts,
persona_threads=persona_threads,
recent_questions=recent_questions,
state.slots = interview_meta.write(
slots,
control=InterviewControlState(
known_facts=known_facts,
persona_threads=persona_threads,
recent_questions=recent_questions,
),
)
await db.commit()
await db.refresh(state)