개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -79,6 +79,8 @@ class TurnContext:
state_before: SessionState
learner_text_raw: str
learner_text_masked: str = ""
counselor_identity: Optional[str] = None
client_identity: Optional[str] = None
crisis: Optional[guardrail.CrisisResult] = None
state_after: Optional[SessionState] = None
messages: list[EngineMessage] = field(default_factory=list)
@ -133,6 +135,7 @@ def prepare_turn(
card: PersonaCard,
state: SessionState,
learner_text: str,
learner_identity: Optional[str] = None,
memory: Optional[TurnMemory] = None,
theory_mode: Optional[str] = None,
eval_rapport_signal: Optional[float] = None,
@ -146,23 +149,42 @@ def prepare_turn(
state_machine 경량 휴리스틱으로 라포 신호를 추정한다.
"""
turn_memory = memory or TurnMemory()
client_identity = card.display_name
ctx = TurnContext(
session_id=session_id,
case_id=case_id,
persona=card,
state_before=state,
learner_text_raw=learner_text,
counselor_identity=learner_identity,
client_identity=client_identity,
memory=TurnMemory(
recall_summary=_mask_optional_text(turn_memory.recall_summary),
pinned_facts=_mask_text_list(turn_memory.pinned_facts),
recent_turns=_mask_recent_turns(turn_memory.recent_turns),
recall_summary=_mask_optional_text(
turn_memory.recall_summary,
counselor_identity=learner_identity,
client_identity=client_identity,
),
pinned_facts=_mask_text_list(
turn_memory.pinned_facts,
counselor_identity=learner_identity,
client_identity=client_identity,
),
recent_turns=_mask_recent_turns(
turn_memory.recent_turns,
counselor_identity=learner_identity,
client_identity=client_identity,
),
kb_behavior_cues=list(turn_memory.kb_behavior_cues or []),
),
theory_mode=theory_mode,
)
# 1) 입력 가드레일 — PII 마스킹 + 위기분류
mask = guardrail.mask_pii(learner_text)
mask = guardrail.mask_role_identities(
learner_text,
counselor_identity=learner_identity,
client_identity=client_identity,
)
ctx.learner_text_masked = mask.text_masked
ctx.crisis = guardrail.classify_crisis(learner_text, speaker_is_persona_context=True)
@ -210,21 +232,51 @@ def prepare_turn(
return ctx
def _mask_optional_text(text: Optional[str]) -> Optional[str]:
def _mask_optional_text(
text: Optional[str],
*,
counselor_identity: Optional[str] = None,
client_identity: Optional[str] = None,
) -> Optional[str]:
if text is None:
return None
return guardrail.mask_pii(text).text_masked
return guardrail.mask_role_identities(
text,
counselor_identity=counselor_identity,
client_identity=client_identity,
).text_masked
def _mask_text_list(values: Optional[list[str]]) -> list[str]:
return [guardrail.mask_pii(value).text_masked for value in (values or [])]
def _mask_text_list(
values: Optional[list[str]],
*,
counselor_identity: Optional[str] = None,
client_identity: Optional[str] = None,
) -> list[str]:
return [
guardrail.mask_role_identities(
value,
counselor_identity=counselor_identity,
client_identity=client_identity,
).text_masked
for value in (values or [])
]
def _mask_recent_turns(turns: Optional[list[dict[str, str]]]) -> list[dict[str, str]]:
def _mask_recent_turns(
turns: Optional[list[dict[str, str]]],
*,
counselor_identity: Optional[str] = None,
client_identity: Optional[str] = None,
) -> list[dict[str, str]]:
masked: list[dict[str, str]] = []
for turn in turns or []:
item = dict(turn)
item["text"] = guardrail.mask_pii(str(item.get("text", ""))).text_masked
item["text"] = guardrail.mask_role_identities(
str(item.get("text", "")),
counselor_identity=counselor_identity,
client_identity=client_identity,
).text_masked
masked.append(item)
return masked
@ -427,6 +479,7 @@ async def run_turn_stream(
flagged = False
output_error: str | None = None
stream_meta: dict[str, Any] = {}
gateway_done = False
previous_client_reply = _latest_client_reply(ctx.memory.recent_turns)
if ctx.crisis is not None and ctx.crisis.escalate:
flagged = True
@ -471,6 +524,7 @@ async def run_turn_stream(
payload = packet.payload
if isinstance(payload, StreamDoneEvent):
stream_meta = payload.model_dump()
gateway_done = True
break
payload = packet.payload
@ -479,6 +533,12 @@ async def run_turn_stream(
text_piece = payload.text
accumulated += text_piece
if not gateway_done:
# 토큰 일부 또는 빈 본문 뒤 연결이 끊겨도 성공 done을 합성하지 않는다.
# 라우트는 이 error 이벤트를 전달하고 durable turn을 저장하지 않는다.
yield StreamEvent("error", {"detail": "client_stream_incomplete"})
return
scenario_leakage = rupture_scenario_director.contains_internal_scenario_leakage(
accumulated
)