세션 계약과 메모리 경계 보강

This commit is contained in:
Yun Chan 2026-06-28 23:52:18 +09:00
parent 391639c1de
commit 2bb052f624
12 changed files with 836 additions and 116 deletions

View file

@ -121,7 +121,6 @@ _RECALL_CACHE: dict[str, memory.RecallContext] = {}
# 세션별 KB 증상 행동단서(회기 1회 산출·캐시). 빈 list 캐시 = 회기 내 재시도 안 함(안정성).
_KB_CUES_CACHE: dict[str, list[str]] = {}
_RAG_WARM_SEMAPHORE = asyncio.Semaphore(1)
_LEARNER_VISIBLE_AI_ROLE = "counselor"
# ────────────────────────────────────────────────────────────────────────────
# RAG 배선 헬퍼 — 내담자(CLIENT) 뷰. 임베더/KB/DB 풀 미가용 시 빈 값으로 graceful
@ -244,12 +243,6 @@ async def _ensure_kb_cues(session_id: str, card) -> list[str]:
return cues
async def _load_prev_case_summary(case_id: str) -> Optional[dict]:
"""직전 회기 요약(case 스코프) → build_recall_context 입력. 미존재/미가용 시 None."""
case_memory = await _load_case_memory(case_id)
return case_memory.get("prev_summary")
async def _load_case_memory(case_id: str) -> dict:
"""case-level 큰그림 + 직전 요약 + client-visible pinned fact를 한 번에 읽는다."""
empty = {"case_digest": None, "prev_summary": None, "pinned_facts": []}
@ -387,6 +380,30 @@ async def ensure_recall_context(sess: InProcSession) -> memory.RecallContext:
return recall
async def _prepare_turn_context(
*,
session_id: str,
sess: InProcSession,
learner_text: str,
) -> orchestrator.TurnContext:
recall = await ensure_recall_context(sess)
kb_cues = _KB_CUES_CACHE.get(session_id) or [] # 비차단: warm 전이면 빈 단서(graceful)
ctx = orchestrator.prepare_turn(
session_id=session_id,
case_id=sess.case_id,
card=sess.persona,
state=sess.state,
learner_text=learner_text,
recall_summary=recall.recall_summary,
pinned_facts=recall.pinned_facts,
recent_turns=sess.recent_turns(visible_to="client"),
kb_behavior_cues=kb_cues,
theory_mode=sess.theory_mode,
)
assert ctx.state_after is not None
return ctx
async def _warm_rag_caches(session_id: str, case_id: str, card) -> None:
"""RAG 회상·KB 행동단서를 **백그라운드**로 산출해 캐시한다(요청 경로 비차단).
@ -1032,22 +1049,11 @@ async def submit_turn(
"""Submit one trainee utterance and return the generated client reply."""
principal = _ensure_learner(principal)
sess = await _load_session_or_404(session_id, principal)
recall = await ensure_recall_context(sess)
kb_cues = _KB_CUES_CACHE.get(session_id) or [] # 비차단: warm 전이면 빈 단서(graceful)
ctx = orchestrator.prepare_turn(
ctx = await _prepare_turn_context(
session_id=session_id,
case_id=sess.case_id,
card=sess.persona,
state=sess.state,
learner_text=body.text,
recall_summary=recall.recall_summary,
pinned_facts=recall.pinned_facts,
recent_turns=sess.recent_turns(visible_to="client"),
kb_behavior_cues=kb_cues,
theory_mode=sess.theory_mode,
sess=sess,
)
assert ctx.state_after is not None
try:
result = await orchestrator.run_turn_generate(
@ -1159,22 +1165,11 @@ async def stream_turn(
"""Stream a generated client reply for one trainee utterance."""
principal = _ensure_learner(principal)
sess = await _load_session_or_404(session_id, principal)
recall = await ensure_recall_context(sess)
kb_cues = _KB_CUES_CACHE.get(session_id) or [] # 비차단: warm 전이면 빈 단서(graceful)
ctx = orchestrator.prepare_turn(
ctx = await _prepare_turn_context(
session_id=session_id,
case_id=sess.case_id,
card=sess.persona,
state=sess.state,
learner_text=body.text,
recall_summary=recall.recall_summary,
pinned_facts=recall.pinned_facts,
recent_turns=sess.recent_turns(visible_to="client"),
kb_behavior_cues=kb_cues,
theory_mode=sess.theory_mode,
sess=sess,
)
assert ctx.state_after is not None
async def event_generator():
last_beat = asyncio.get_running_loop().time()
@ -1229,7 +1224,7 @@ async def end_session(
session_id=session_id,
case_id=sess.case_id,
session_no=sess.session_no,
masked_turns=sess.masked_turns(),
masked_turns=sess.masked_turns(visible_to="client"),
prev_rapport_credit=sess.prev_rapport_credit,
open_threads=recall.open_threads,
)