vignette/apps/api/app/taxonomy.py
Yun Chan 778e8526d4 세션 평가·라이브코치·교수자 분석 라운드 마감 + 문서 정리 + 코드품질 리팩터
- 누적 작업트리 커밋: 회기 평가 복구·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
2026-07-02 02:50:36 +09:00

292 lines
18 KiB
Python

"""발화 라벨 taxonomy + annotation 데이터 모델 (0615 파란색 라벨 → 구조화 스키마).
근거:
- 데이터/README.md: 0615 축어록 파란색(#3057B9) 주석 = 3종 혼재
(기법 태그 / 내담자 상태 태그 / 슈퍼바이저 논평).
- MASTERPLAN.md §3.1: 0615.hwpx 직접 파싱 결과 파랑 라벨 89개·77종, 한 셀에 3종 혼재
-> 학습 신호 오염 방지를 위해 **3개 분리 필드로 격상**이 스키마 설계의 출발점.
- MASTERPLAN.md §3.2: technique_label_def / client_state_def / stage_def 코드테이블 +
turn_technique / turn_client_state 다대다 + supervisor_comment(kind: rationale|critique).
- MEMORY_KNOWLEDGE_PERSONA_DESIGN.md §3.6: kb.chunk.label_id 가 taxonomy 라벨을 FK 참조.
이 모듈은 평가 AI 정답셋(golden label) + 페르소나 상태전이 정답의 **단일 코드 원천**이다.
DB 코드테이블(technique_label_def 등)과 동기화 대상 -> seed 시 enum.value 를 code 컬럼에 적재.
설계 원칙:
- 위계적(hierarchical): 기법은 군집(category) 아래에 둔다 -> 평가 분포·집계·과다/과소 판정용.
- 조작적 정의(operational definition)는 docs/taxonomy.md 가 SoT, 여기엔 enum + 메타만.
- 라벨은 **버전드**(코드 불변, 새 라벨 추가는 append-only) -> 골든셋 재현성 보존.
원본(0615) 자체는 절대 적재하지 않는다(미성년·자살사고, .gitignore data/raw). 합성 변형만 예시화.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
TAXONOMY_VERSION = "1.0.0" # 라벨 코드 불변 보장 버전. 새 라벨 추가 시 minor++.
# ════════════════════════════════════════════════════════════════════════════
# 1. 회기 단계 (stage) — 라벨 enum 단일 정의(SoT). 전이 로직은 상태머신 소유(MASTERPLAN §2.2).
# services.state_machine 이 이 Stage 를 re-export 한다(중복 정의 제거).
# ════════════════════════════════════════════════════════════════════════════
class Stage(str, Enum):
"""상담 회기 단계 라벨(SoT). 결정론 전이는 services.state_machine 이 소유(LLM 아님)."""
RAPPORT = "라포" # 라포 형성 — 안전감·관계 구축, 비밀보장 구조화
EXPLORE = "탐색" # 호소문제·정서·인지·위험요인 탐색
INTERVENE = "개입" # 직면·해석·인지재구성·교정적 정서체험
CLOSE = "정리" # 요약·과제·다음 회기 연결·종결
# ════════════════════════════════════════════════════════════════════════════
# 2. 화자 (speaker) — 발화의 최소 원자 출처
# ════════════════════════════════════════════════════════════════════════════
class Speaker(str, Enum):
"""발화 화자. 0615 축어록의 '상N'/'내N' 라벨에 대응."""
COUNSELOR = "counselor" # 상담자(수련생 또는 상담사 AI)
CLIENT = "client" # 내담자(가상내담자 AI)
def speaker_ko_label(speaker: str | None) -> str:
"""화자 코드 → 프롬프트용 한글 라벨(SoT). counselor→상담자, 그 외→내담자."""
return "상담자" if speaker == Speaker.COUNSELOR.value else "내담자"
# ════════════════════════════════════════════════════════════════════════════
# 3. 기법 군집 (TechniqueCategory) — 위계 상위층
# ════════════════════════════════════════════════════════════════════════════
class TechniqueCategory(str, Enum):
"""상담자 기법의 상위 군집. 평가 분포·과다/과소 집계·리뷰 사이드 게이지의 축."""
RELATIONAL = "relational" # 관계 형성: 공감·반영·타당화·홀딩·자기공개·유머
EXPLORATORY = "exploratory" # 탐색: 촉진질문·탐색·위험사정·동의/동기 확인
INTERVENTION = "intervention" # 개입: 직면·해석·인지재구성·교정적 정서체험
STABILIZING = "stabilizing" # 안정/지지: 안정화·희망고취·강화·정상화
STRUCTURING = "structuring" # 구조화: 상담원칙/비밀보장 설명·과제·심리교육
# ════════════════════════════════════════════════════════════════════════════
# 4. 기법 라벨 (Technique) — 위계 하위층 (0615 파란색 기법 태그 origin)
# ════════════════════════════════════════════════════════════════════════════
class Technique(str, Enum):
"""발화별 상담 기법 라벨(복수 부착 가능). 값(value)=DB code, 0615 표면 라벨에서 정규화.
조작적 정의·예시는 docs/taxonomy.md 참조. 여기 값은 안정적 머신 코드.
"""
# ── RELATIONAL ──
EMPATHY = "empathy" # 공감 (0615: "공감")
REFLECTION = "reflection" # 반영 (0615: "반영", "정서 반영", "내담자의 반응을 반영함")
VALIDATION = "validation" # 타당화 (0615: "타당화", "감정 노출의 타당화")
HOLDING = "holding" # 홀딩 — 침묵 견디기/기다려 줌 (0615: "홀딩", "탐색과 홀딩")
SELF_DISCLOSURE = "self_disclosure" # 자기공개 (0615: "라포형성을 위한 상담자의 자기 개방")
AFFECT_CONVEYANCE = "affect_conveyance" # 상담자 정서 전달 (0615: "상담자의 정서 전달")
HUMOR = "humor" # 유머 (0615: "유머")
# ── EXPLORATORY ──
EXPLORATION = "exploration" # 탐색 (0615: "탐색", "탐색질문")
FACILITATIVE_QUESTION = "facilitative_question" # 촉진/개방 질문 (0615: "촉진", "참여 촉진 질문")
RISK_ASSESSMENT = "risk_assessment" # 위험요인/자해·자살 탐색 (0615: "위험요인 탐색", "자해 위험 및 경험 탐색")
CONSENT_MOTIVATION_CHECK = "consent_motivation_check" # 동의/동기 확인 (0615: "동의확인, 동기수준 확인")
OPINION_CHECK = "opinion_check" # 내담자 의견·반응 확인 (0615: "내담자 의견 확인", "주호소 문제 재확인")
# ── INTERVENTION ──
CONFRONTATION = "confrontation" # 직면 (0615: "기초자료를 활용하여 반응의 불일치에 직면시킴")
INTERPRETATION = "interpretation" # 해석 (0615: "모순의 의미를 해석함", "비언어적 자극 해석", "행동의 재해석")
# ── STABILIZING ──
STABILIZATION = "stabilization" # 안정화 (0615: "안정화")
HOPE_INSTILLATION = "hope_instillation" # 희망고취 (0615: "동기부여, 희망고취", "희망 고취, 욕구 반영")
REINFORCEMENT = "reinforcement" # 강화 (0615: "강화")
NORMALIZATION = "normalization" # 정상화 (0615: "감정 반응을 노출하는 것을 정상화함")
# ── STRUCTURING ──
PRINCIPLE_EXPLANATION = "principle_explanation" # 상담원칙/비밀보장 설명 (0615: "상담원칙에 대한 설명", "비밀보장 제외 원칙 설명")
PSYCHOEDUCATION = "psychoeducation" # 심리교육/보편교범 전달 (0615: "보편적 교범 전달, 주관적 해석과 구별")
HOMEWORK = "homework" # 과제 부여 (0615: "다음 회기까지 해 와야 하는 과제를 설명")
# 기법 -> 군집 매핑 (위계). 집계·분포·과다/과소 판정에 사용.
TECHNIQUE_CATEGORY: dict[Technique, TechniqueCategory] = {
Technique.EMPATHY: TechniqueCategory.RELATIONAL,
Technique.REFLECTION: TechniqueCategory.RELATIONAL,
Technique.VALIDATION: TechniqueCategory.RELATIONAL,
Technique.HOLDING: TechniqueCategory.RELATIONAL,
Technique.SELF_DISCLOSURE: TechniqueCategory.RELATIONAL,
Technique.AFFECT_CONVEYANCE: TechniqueCategory.RELATIONAL,
Technique.HUMOR: TechniqueCategory.RELATIONAL,
Technique.EXPLORATION: TechniqueCategory.EXPLORATORY,
Technique.FACILITATIVE_QUESTION: TechniqueCategory.EXPLORATORY,
Technique.RISK_ASSESSMENT: TechniqueCategory.EXPLORATORY,
Technique.CONSENT_MOTIVATION_CHECK: TechniqueCategory.EXPLORATORY,
Technique.OPINION_CHECK: TechniqueCategory.EXPLORATORY,
Technique.CONFRONTATION: TechniqueCategory.INTERVENTION,
Technique.INTERPRETATION: TechniqueCategory.INTERVENTION,
Technique.STABILIZATION: TechniqueCategory.STABILIZING,
Technique.HOPE_INSTILLATION: TechniqueCategory.STABILIZING,
Technique.REINFORCEMENT: TechniqueCategory.STABILIZING,
Technique.NORMALIZATION: TechniqueCategory.STABILIZING,
Technique.PRINCIPLE_EXPLANATION: TechniqueCategory.STRUCTURING,
Technique.PSYCHOEDUCATION: TechniqueCategory.STRUCTURING,
Technique.HOMEWORK: TechniqueCategory.STRUCTURING,
}
# 한글 표시명(UI 라벨 칩·리뷰 화면). docs/taxonomy.md 와 일치.
TECHNIQUE_KO: dict[Technique, str] = {
Technique.EMPATHY: "공감",
Technique.REFLECTION: "반영",
Technique.VALIDATION: "타당화",
Technique.HOLDING: "홀딩",
Technique.SELF_DISCLOSURE: "자기공개",
Technique.AFFECT_CONVEYANCE: "정서 전달",
Technique.HUMOR: "유머",
Technique.EXPLORATION: "탐색",
Technique.FACILITATIVE_QUESTION: "촉진질문",
Technique.RISK_ASSESSMENT: "위험사정",
Technique.CONSENT_MOTIVATION_CHECK: "동의·동기 확인",
Technique.OPINION_CHECK: "의견 확인",
Technique.CONFRONTATION: "직면",
Technique.INTERPRETATION: "해석",
Technique.STABILIZATION: "안정화",
Technique.HOPE_INSTILLATION: "희망고취",
Technique.REINFORCEMENT: "강화",
Technique.NORMALIZATION: "정상화",
Technique.PRINCIPLE_EXPLANATION: "상담원칙 설명",
Technique.PSYCHOEDUCATION: "심리교육",
Technique.HOMEWORK: "과제 부여",
}
# ════════════════════════════════════════════════════════════════════════════
# 5. 내담자 상태 태그 (ClientState) — 0615 파란색 상태 태그 origin
# ════════════════════════════════════════════════════════════════════════════
class ClientState(str, Enum):
"""내담자 발화/비언어에 부착되는 상태 태그(복수 가능).
페르소나 상태전이(저항 엔진) 정답 신호 + 평가 AI 의 '반응 읽기' 채점 근거.
0615 파란색 상태 태그에서 정규화.
"""
INVOLUNTARY = "involuntary" # 비자발적 태도 (0615: "(아니오)는 비자발적 태도", "(쓴웃음)은 비자발적 태도")
DEFENSIVE = "defensive" # 방어 (0615: "방어", "부모자녀 관계에 대한 방어적 태도")
SUICIDAL_IDEATION_ADMIT = "suicidal_ideation_admit" # 자살사고 인정 (0615: "자살사고 인정")
NEGATIVE_SELF_PERCEPTION = "negative_self_perception" # 부정적 자기인식 (0615: "부정적 자기인식", "부정적인 자기인식")
CONFLICTED = "conflicted" # 갈등 상태 (0615: "갈등 상태")
LACK_OF_CONFIDENCE = "lack_of_confidence" # 확신/자신감 부족 (0615: "확신의 부족")
AFFECT_CONTACT = "affect_contact" # 정서와 접촉/표현 (0615: "정서와 접촉함", "정서와 사고의 표현")
THOUGHT_ORGANIZING = "thought_organizing" # 생각을 정리함 (0615: "생각을 정리함")
RESPONDS_TO_EXPLORATION = "responds_to_exploration" # 탐색에 반응함 (0615: "상담자의 탐색에 반응함")
EXPRESSES_PLAN = "expresses_plan" # 자기 계획/욕구 표현 (0615: "자신의 계획을 표현함")
DEFENSE_LOOSENING = "defense_loosening" # 방어가 서서히 풀림 (0615: "방어가 서서히 풀어지고 있음")
CLIENT_STATE_KO: dict[ClientState, str] = {
ClientState.INVOLUNTARY: "비자발적 태도",
ClientState.DEFENSIVE: "방어",
ClientState.SUICIDAL_IDEATION_ADMIT: "자살사고 인정",
ClientState.NEGATIVE_SELF_PERCEPTION: "부정적 자기인식",
ClientState.CONFLICTED: "갈등 상태",
ClientState.LACK_OF_CONFIDENCE: "확신 부족",
ClientState.AFFECT_CONTACT: "정서 접촉/표현",
ClientState.THOUGHT_ORGANIZING: "생각 정리",
ClientState.RESPONDS_TO_EXPLORATION: "탐색에 반응",
ClientState.EXPRESSES_PLAN: "계획/욕구 표현",
ClientState.DEFENSE_LOOSENING: "방어 완화",
}
# ════════════════════════════════════════════════════════════════════════════
# 6. 슈퍼바이저 논평 (SupervisorComment) — 0615 논평 origin
# ════════════════════════════════════════════════════════════════════════════
class CommentKind(str, Enum):
"""슈퍼바이저 논평 종류 (MASTERPLAN §3.1, §3.2: rationale vs critique 분리).
윤찬 지시: '의도와 다른 부분'(intent_deviation)은 1급 시민.
"""
RATIONALE = "rationale" # 근거/의도 설명 — 왜 이 반응이 적절한가 (0615 회색 '·' 논평 + 일부 파랑)
CRITIQUE = "critique" # 개선점/주의 — 과도/부족/평가적 시각 등 (0615: "과도한 자기개방일 수 있음")
@dataclass(slots=True)
class SupervisorComment:
"""발화에 달린 슈퍼바이저 논평 한 건.
critique 인 경우 intent_deviation 으로 '의도(expected) vs 실제(actual)' 격차를 구조화.
예) 0615 "비언어적 반응의 반영이 더 되었으면" -> dimension=reflection, severity=minor.
"""
kind: CommentKind
text: str
# critique 전용(선택): 평가 차원·기대·실제·심각도. README/§3.2 intent_deviation JSONB 대응.
dimension: Optional[str] = None # 관련 평가 차원 (예: "reflection", "self_disclosure", "pacing")
expected: Optional[str] = None # 권장된 반응/의도
actual: Optional[str] = None # 실제 나타난 반응
severity: Optional[str] = None # "minor" | "moderate" | "major"
author: str = "ai" # "ai"(자동 1R) | "supervisor"(교수 검수 2R)
# ════════════════════════════════════════════════════════════════════════════
# 7. 발화 annotation (Annotation) — 3축 분리 최종 레코드 (golden 단위)
# ════════════════════════════════════════════════════════════════════════════
@dataclass(slots=True)
class Annotation:
"""발화 1개의 구조화 annotation = golden_schema.jsonl 한 줄에 대응.
README 스키마:
{"발화ID", "화자", "단계", "발화텍스트", "기법라벨"[], "내담자상태"[], "슈퍼바이저논평"}
를 3축 분리(기법/내담자상태/논평)로 격상한 머신 표현.
부착 규칙(평가 AI 정답셋 일관성):
- techniques 는 speaker=COUNSELOR 발화에만 부착(내담자 발화엔 빈 리스트).
- client_states 는 speaker=CLIENT 발화(또는 비언어 표현)에만 부착.
- comments 는 두 화자 모두 가능(슈퍼비전 관점).
- nonverbal: 괄호 표기 비언어 단서("(쓴웃음)","(침묵 10초)") -> 검증/상태판정 보조.
"""
utterance_id: str # 예 "0615-C-001"(상1), "0615-K-003"(내3). 화자+seq 인코딩
speaker: Speaker
stage: Stage
text: str # 발화 텍스트 (예시는 합성 변형, 원문 미적재)
seq: int # 회기 내 발화 순서(0-based)
techniques: list[Technique] = field(default_factory=list) # 기법 라벨(복수)
client_states: list[ClientState] = field(default_factory=list) # 내담자 상태(복수)
comments: list[SupervisorComment] = field(default_factory=list) # 슈퍼바이저 논평(복수)
nonverbal: list[str] = field(default_factory=list) # 비언어 단서 원문 표기
source: str = "0615_synthetic" # provenance: 0615 합성 변형(원문 미적재, F-05)
def __post_init__(self) -> None:
# 정합성: 기법은 상담자, 상태는 내담자에만 (평가 신호 오염 방지).
if self.techniques and self.speaker is not Speaker.COUNSELOR:
raise ValueError(
f"techniques 는 상담자 발화에만 부착 가능: {self.utterance_id}"
)
if self.client_states and self.speaker is not Speaker.CLIENT:
raise ValueError(
f"client_states 는 내담자 발화에만 부착 가능: {self.utterance_id}"
)
def technique_categories(self) -> set[TechniqueCategory]:
"""부착된 기법들의 군집 집합 (분포 집계용)."""
return {TECHNIQUE_CATEGORY[t] for t in self.techniques}
__all__ = [
"TAXONOMY_VERSION",
"Stage",
"Speaker",
"speaker_ko_label",
"TechniqueCategory",
"Technique",
"TECHNIQUE_CATEGORY",
"TECHNIQUE_KO",
"ClientState",
"CLIENT_STATE_KO",
"CommentKind",
"SupervisorComment",
"Annotation",
]