C-001 임상 검토 게이트 보강

This commit is contained in:
Yun Chan 2026-08-28 19:04:17 +09:00
parent 391fb9f4d0
commit 6988280b30
18 changed files with 1896 additions and 67 deletions

View file

@ -23,8 +23,9 @@ from difflib import SequenceMatcher
from enum import Enum
from typing import Iterable, Protocol
from .state_machine import IDEATION_STAGE_CAP, clamp_ideation_stage
# ── 출력 가드레일 상한 (R5) ──────────────────────────────
IDEATION_STAGE_CAP = 3 # 내담자 발화/상태가 넘을 수 없는 자살사고 단계 상한
CRISIS_HOTLINE_NUMBER = "109"
CRISIS_HOTLINE_LABEL = "자살예방상담전화 109"
CRISIS_RESOURCE_MESSAGE = (
@ -648,8 +649,8 @@ def sanitize_client_reply(
"""내담자 AI 응답 출력 가드레일.
1. 자살/자해 수단·방법 정보가 있으면 needs_regeneration=True(차단, 재생성 신호).
2. ideation_stage > IDEATION_STAGE_CAP(3) 이면 안전 상한 위반으로 기록(상태머신이 상한 강제하나
이중방어). 응답 자체 텍스트는 정서 표현이면 유지.
2. ideation_stage > IDEATION_STAGE_CAP(3) 이면 상태머신 계약 위반으로 차단하고
needs_regeneration=True를 반환한다(영속 상태 상한에 대한 이중방어).
"""
reasons: list[str] = []
blocked = False
@ -678,6 +679,7 @@ def sanitize_client_reply(
if ideation_stage > IDEATION_STAGE_CAP:
reasons.append(f"ideation_over_cap:{ideation_stage}>{IDEATION_STAGE_CAP}")
blocked = True
needs_regen = True
return OutputGuardResult(
text=humanize_pii_placeholders(text),
@ -689,7 +691,7 @@ def sanitize_client_reply(
def clamp_ideation(stage: int) -> int:
"""ideation_stage 를 안전 상한(3)으로 클램프 (R5)."""
return max(1, min(IDEATION_STAGE_CAP, stage))
return clamp_ideation_stage(stage)
def crisis_resource() -> dict[str, str]:

View file

@ -46,6 +46,15 @@ STAGE_ADVANCE_RAPPORT: dict[Stage, float] = {
Stage.INTERVENE: 0.55, # 개입 작업이 진행됨
}
# 내담자 발화와 회기 영속 상태가 공유하는 자살사고 단계의 절대 범위(R5).
# 상태를 소유하는 이 모듈이 단일 정의를 제공하고, 출력 가드레일도 이를 재사용한다.
IDEATION_STAGE_CAP = 3
def clamp_ideation_stage(stage: int) -> int:
"""자살사고 단계를 영속 가능한 안전 범위(1..3)로 제한한다."""
return max(1, min(IDEATION_STAGE_CAP, int(stage)))
@dataclass(slots=True)
class SessionState:
@ -59,10 +68,15 @@ class SessionState:
effective_openness: float = 0.15
rapport_credit: float = 0.0 # 회기 누적(회기말 0.7 이월)
resistance: float = 0.65 # base_resistance 에서 시작, decay 로 완화
ideation_stage: int = 1 # 1~5 (출력 가드레일 상한 3)
ideation_stage: int = 1 # 영속 상태 절대 범위 1..IDEATION_STAGE_CAP
turns_in_stage: int = 0 # 현재 단계 체류 턴 수
affect_state: dict[str, float] = field(default_factory=dict)
def __post_init__(self) -> None:
# DB row나 테스트 fixture가 이 dataclass를 직접 만들더라도 과상한 값이
# prompt/guardrail 입력으로 잠깐 노출되지 않게 생성 경계에서 정규화한다.
self.ideation_stage = clamp_ideation_stage(self.ideation_stage)
def snapshot(self) -> dict:
"""무손실 carry-over용 snapshot (memory.end_state). 코드 복사, LLM 미경유."""
return {
@ -186,10 +200,11 @@ def evolve(
else:
resistance = min(1.0, state.resistance - 0.06 * rapport_signal) # signal<0 → 증가
# 5) ideation 보수적 유지(절대 내려가지 않음, 안전)
ideation_stage = state.ideation_stage
# 5) ideation 보수적 유지(절대 내려가지 않음, 안전). 이미 저장된 과상한 상태와
# 새 관측값을 각각 먼저 제한해 과거 drift가 다음 snapshot으로 전파되지 않게 한다.
ideation_stage = clamp_ideation_stage(state.ideation_stage)
if ideation_observed is not None:
ideation_stage = max(state.ideation_stage, ideation_observed)
ideation_stage = max(ideation_stage, clamp_ideation_stage(ideation_observed))
# 3) 개방도 재계산
eff = compute_effective_openness(
@ -250,16 +265,18 @@ def init_state(
stage = Stage.RAPPORT
resistance = params.base_resistance
rapport_credit = 0.0
ideation_stage = params.ideation_baseline
ideation_baseline = clamp_ideation_stage(params.ideation_baseline)
ideation_stage = ideation_baseline
if carry:
rapport_credit = float(carry.get("rapport_credit", 0.0)) * 0.7 # P2 이월
# inter-session drift: 라포가 쌓였으면 저항 소폭 완화된 채로 재시작
prev_resist = float(carry.get("resistance", params.base_resistance))
resistance = _clamp01((prev_resist + params.base_resistance) / 2.0)
ideation_stage = max(
int(carry.get("ideation_stage", params.ideation_baseline)), params.ideation_baseline
carried_ideation = clamp_ideation_stage(
int(carry.get("ideation_stage", ideation_baseline))
)
ideation_stage = max(carried_ideation, ideation_baseline)
eff = compute_effective_openness(
stage=stage,
@ -284,8 +301,10 @@ __all__ = [
"Stage",
"STAGE_BASE_OPENNESS",
"STAGE_ORDER",
"IDEATION_STAGE_CAP",
"SessionState",
"OpennessParams",
"clamp_ideation_stage",
"estimate_rapport_signal",
"compute_effective_openness",
"next_stage",