세션 평가·라이브코치·교수자 분석 라운드 마감 + 문서 정리 + 코드품질 리팩터

- 누적 작업트리 커밋: 회기 평가 복구·durable 저장, 라이브 코치 이력/근거, 교수자 학생분석, 음성 비언어 메타, PII 마스킹, 운영 티켓/헬스 등
- 문서: 완료 기록 docs/archive/ 냉동 보관, docs/ 단일 인덱스(docs/README.md)+통합 TODO(docs/TODO.md)로 정리
- 리팩터(행위 보존): Stage enum SSOT(taxonomy 소유·state_machine re-export), store recent/masked_turns 중복 제거, speaker_ko_label 단일 헬퍼, _list_sessions N+1 제거(state/turns 배치 + 턴평가 하이드레이션 배치)
- 검증: 백엔드 pytest 352 passed, _list_sessions E2E chromium-single-run 2 passed
This commit is contained in:
Yun Chan 2026-07-02 02:50:36 +09:00
parent 7c41c3ce79
commit 778e8526d4
108 changed files with 6457 additions and 455 deletions

View file

@ -25,6 +25,7 @@ from ..contracts.engine_gateway import structured_payload_from_response
from ..engine_client import EngineClient, EngineError, EngineMessage, GenerateRequest
from ..paths import repo_root, repo_path
from ..session_read_model import StageLabel, stage_label_or_none
from ..taxonomy import speaker_ko_label
from . import guardrail
if TYPE_CHECKING:
@ -33,6 +34,7 @@ if TYPE_CHECKING:
Tone = Literal["pos", "warn", "neutral"]
CoachStatus = Literal["ready", "degraded"]
CoachCreditEventType = Literal["use", "recharge"]
CoachFocus = Literal[
"rapport",
"exploration",
@ -71,6 +73,35 @@ class LiveCoachSuggestion(BaseModel):
sources: list[LiveCoachSource] = Field(default_factory=list)
safety_note: Optional[str] = None
latency_ms: int = 0
persistence_source: Literal["database", "runtime"] = "database"
quota: Optional["LiveCoachQuota"] = None
credit_events: list["LiveCoachCreditEvent"] = Field(default_factory=list)
class LiveCoachQuota(BaseModel):
"""회기 중 즉시 코칭 사용 가능 횟수."""
remaining: int = Field(ge=0)
max: int = Field(ge=1)
class LiveCoachCreditEvent(BaseModel):
"""코칭 기회 사용/충전 학습 기록."""
event_id: str
session_id: str
turn_seq: int
stage: StageLabel | None = None
event_type: CoachCreditEventType
delta: int
balance: int = Field(ge=0)
reason: str
created_at: str
@field_validator("stage", mode="before")
@classmethod
def _normalize_stage(cls, value: object) -> StageLabel | None:
return stage_label_or_none(value)
class LiveCoachEvent(BaseModel):
@ -313,6 +344,7 @@ def build_rag_index_payloads() -> list[dict[str, Any]]:
"sensitivity": _rag_sensitivity(source, chunk, kb_kind),
"meta": {
"live_coaching_source": True,
"source_title": title,
"source_type": str(chunk.get("source_type") or source.get("source_type") or ""),
"source_version": version_label,
"citation": chunk_citation,
@ -557,14 +589,33 @@ def _grounding_block(grounding: list[LiveCoachGrounding]) -> str:
return "\n".join(lines)
def _mask_prompt_text(value: object) -> str:
return guardrail.mask_pii(str(value or "")).text_masked
def _mask_prompt_value(value: Any) -> Any:
if isinstance(value, str):
return _mask_prompt_text(value)
if isinstance(value, dict):
return {
_mask_prompt_text(key): _mask_prompt_value(child)
for key, child in value.items()
}
if isinstance(value, list):
return [_mask_prompt_value(child) for child in value]
if isinstance(value, tuple):
return [_mask_prompt_value(child) for child in value]
return value
def _messages(item: LiveCoachInput, grounding: list[LiveCoachGrounding]) -> list[EngineMessage]:
learner_masked = guardrail.mask_pii(item.learner_text).text_masked
client_masked = guardrail.mask_pii(item.client_reply or "").text_masked
learner_masked = _mask_prompt_text(item.learner_text)
client_masked = _mask_prompt_text(item.client_reply or "")
recent = "\n".join(
f"{'상담자' if t.get('speaker') == 'counselor' else '내담자'}: {t.get('text', '')}"
f"{speaker_ko_label(t.get('speaker'))}: {_mask_prompt_text(t.get('text', ''))}"
for t in item.recent_turns[-6:]
) or "(최근 맥락 없음)"
evaluation = json.dumps(item.evaluation or {}, ensure_ascii=False)[:1200]
evaluation = json.dumps(_mask_prompt_value(item.evaluation or {}), ensure_ascii=False)[:1200]
system = (
"당신은 심리상담 수련생에게 회기 중 즉시 피드백을 주는 라이브 코치다.\n"
"목표는 지금 흐름을 끊지 않고 다음 상담자 발화 하나를 더 낫게 만드는 것이다.\n\n"
@ -669,6 +720,8 @@ __all__ = [
"LiveCoachEvent",
"LiveCoachGrounding",
"LiveCoachInput",
"LiveCoachCreditEvent",
"LiveCoachQuota",
"LiveCoachSource",
"LiveCoachSuggestion",
"clear_local_source_pack_cache",