전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -136,6 +136,10 @@ class LiveCoachInput(BaseModel):
client_reply: Optional[str] = None
recent_turns: list[dict[str, str]] = Field(default_factory=list)
evaluation: Optional[dict[str, Any]] = None
# 이번 회기 목표 단계(P1 준비 페이지 선택) — 코칭을 회기 목표에 정렬한다.
goal_stages: list[str] = Field(default_factory=list)
# 직전 코칭 요약(title/focus) — 같은 조언 반복을 막는다.
prior_coach: list[dict[str, str]] = Field(default_factory=list)
class LiveCoachGrounding(BaseModel):
@ -529,7 +533,17 @@ def _fallback_suggestion(
focus: CoachFocus = "exploration"
title = "다음 탐색"
message = "내담자 표현을 한 번 반영한 뒤, 방금 말한 장면을 더 구체적으로 물어봐라."
next_line = "방금 말한 그 장면이 언제부터 특히 힘들게 느껴졌는지 조금만 더 들려줄래요?"
# 단계별 기본 다음 발화 — 폴백에서도 회기 흐름에 맞는 제안을 낸다.
stage_next_lines = {
"라포": "오늘 이렇게 시간 내줘서 고마워요. 지금 마음이 어떤지 편한 만큼만 들려줄래요?",
"탐색": "방금 말한 그 장면이 언제부터 특히 힘들게 느껴졌는지 조금만 더 들려줄래요?",
"개입": "그 생각이 올라올 때 몸이나 행동은 어떻게 반응하는지 같이 한번 살펴볼까요?",
"정리": "오늘 나눈 이야기 중에 가장 마음에 남는 것 하나를 같이 정리해 볼까요?",
}
next_line = stage_next_lines.get(
str(item.stage),
"방금 말한 그 장면이 언제부터 특히 힘들게 느껴졌는지 조금만 더 들려줄래요?",
)
crisis = guardrail.classify_crisis(text)
if crisis.kind != guardrail.CrisisKind.NONE:
@ -633,10 +647,21 @@ def _messages(item: LiveCoachInput, grounding: list[LiveCoachGrounding]) -> list
"영역의 구체 행동으로 연결한다. DSM/지침 근거는 상담자 판단을 정렬하는 내부 참조이며, "
"학습자에게는 관찰 가능한 상담 행동과 다음 발화로만 번역한다."
)
goals = ", ".join(item.goal_stages) if item.goal_stages else "(미지정)"
prior = (
"\n".join(
f"- {entry.get('title', '')} (focus: {entry.get('focus', '')})"
for entry in item.prior_coach[-2:]
if entry.get("title")
)
or "(이번 회기 첫 코칭)"
)
user = (
f"[세션] {item.session_id} / turn {item.turn_seq}\n"
f"[내담자] {item.persona_name} ({item.persona_code})\n"
f"[단계] {item.stage} / openness {item.effective_openness:.2f} / 이론 {item.theory_mode}\n\n"
f"[단계] {item.stage} / openness {item.effective_openness:.2f} / 이론 {item.theory_mode}\n"
f"[이번 회기 목표 단계] {goals} — 코칭은 목표 단계 작업에 정렬하고, 목표를 이미 이뤘다면 심화를 제안한다.\n"
f"[직전 코칭]\n{prior}\n(같은 조언을 반복하지 말고 다음 단계를 제시한다)\n\n"
f"[최근 맥락]\n{recent}\n\n"
f"[이번 상담자 발화]\n{learner_masked}\n\n"
f"[이어진 내담자 응답]\n{client_masked or '(아직 없음)'}\n\n"
@ -654,13 +679,14 @@ def _messages(item: LiveCoachInput, grounding: list[LiveCoachGrounding]) -> list
async def _record_llm_audit(
audit_hook: Optional["LlmAuditHook"],
**payload: Any,
) -> None:
) -> bool:
if audit_hook is None:
return
return True
try:
await audit_hook(payload)
result = await audit_hook(payload)
return result is not False
except Exception:
return
return False
async def generate_live_coaching(
@ -691,7 +717,7 @@ async def generate_live_coaching(
)
resp = await engine.generate(req)
latency_ms = int((time.perf_counter() - started) * 1000)
await _record_llm_audit(
audit_ok = await _record_llm_audit(
audit_hook,
session_id=item.session_id,
provider=resp.provider,
@ -702,6 +728,12 @@ async def generate_live_coaching(
inference_geo=resp.inference_geo,
latency_ms=latency_ms,
)
if not audit_ok:
return _fallback_suggestion(
item,
grounding=all_grounding,
reason="응답 검증 기록을 남기지 못했다",
)
payload = structured_payload_from_response(resp)
if payload is None:
return _fallback_suggestion(