Jev 내담자 평가·표현 v2와 속마음 공개

상담자 발화 판정(A)·감정(B)·표현(C) 20문항 질문 세트, 감쇠 없는 이번 턴 반응과 비대칭 기분 전이, 개방도 게이트로 생성 지시를 만들고 ccd.coping_strategy 전달 누락을 고친다.

trace v2와 고정 문구 속마음 요약(migration 24, AI 경로 차단 RLS)을 같은 트랜잭션에 저장하고 피드백 정책이 켜진 경우에만 done·TurnResponse·음성 reply·리뷰로 노출한다. 회기 화면 속마음 보기 토글, 리뷰 접힘 블록, 관리자 감정 관측 v2 표시를 추가한다.
This commit is contained in:
Yun Chan 2026-09-30 13:23:09 +09:00
parent 36cb847e38
commit 29c406d89f
39 changed files with 5726 additions and 343 deletions

View file

@ -23,7 +23,7 @@ import time
from dataclasses import dataclass, field, replace
from typing import Any, AsyncIterator, Awaitable, Callable, Optional
from ..contracts.client_affect import ClientAffectTraceV1
from ..contracts.client_affect import ClientAffectTraceV1, ClientAffectTraceV2, ClientInnerReactionV1
from ..config import settings
from ..engine_client import (
EngineClient,
@ -95,8 +95,12 @@ class TurnContext:
scenario_directive: Optional[rupture_scenario_director.ScenarioDirective] = None
# 외부 감정 평가의 안전한 provenance. 원문·점수·확률은 넣지 않는다.
client_affect_metadata: Optional[dict[str, Any]] = None
# 관리자 관측 전용 Jev 전이 trace. 공개 결과나 provider event에는 넣지 않는다.
client_affect_trace: ClientAffectTraceV1 | None = None
# 관리자 관측 전용 Jev 전이 trace(v1|v2). 공개 결과나 provider event에는 넣지 않는다.
client_affect_trace: ClientAffectTraceV1 | ClientAffectTraceV2 | None = None
# v2 생성 지시(§7). L3 '정서 연기 지시' 줄을 대체한다. legacy·v1 경로에선 None.
client_affect_directive: Optional[str] = None
# 학습자·교수자용 속마음 요약(§8.2). 이 패킷에서는 저장·노출하지 않고 조립만 한다.
client_inner_reaction: ClientInnerReactionV1 | None = None
def to_state_context(self) -> PersonaStateContext:
st = self.state_after or self.state_before
@ -107,6 +111,7 @@ class TurnContext:
rapport_credit=st.rapport_credit,
ideation_stage=st.ideation_stage,
affect_state=st.affect_state,
affect_directive=self.client_affect_directive,
)
@ -322,20 +327,24 @@ def _rebuild_persona_messages(ctx: TurnContext) -> None:
)
def _minimal_persona_context(card: PersonaCard) -> dict[str, Any]:
"""Jev가 반응을 해석할 최소 페르소나 단서만 고른다."""
def _client_profile_inputs(card: PersonaCard) -> dict[str, Any]:
"""v2 Jev state의 client_profile 원자료(마스킹 전)를 카드에서 고른다.
v1은 ccd["coping"]을 읽었지만 카드는 coping_strategy를 쓰므로 대처 방식이
한 번도 전달되지 않았다(§1 근거 5). client_profile.coping_strategy로 고친다.
"""
ccd = card.ccd or {}
triggers = card.triggers or {}
return {
"big5": card.big5,
"resistance": card.resistance,
"speech_style": card.speech_style,
"presenting": card.presenting,
"history": card.history,
"ccd": {
key: card.ccd.get(key)
for key in ("core_belief", "automatic_thought", "coping")
if key in card.ccd
},
"triggers": card.triggers,
"core_belief": ccd.get("core_belief"),
"automatic_thought": ccd.get("automatic_thought"),
"coping_strategy": ccd.get("coping_strategy"),
"big5": card.big5,
"sore_spots": list(triggers.get("sore_spots") or []),
"forbidden": list(triggers.get("forbidden") or []),
"speech_style": card.speech_style,
}
@ -363,14 +372,20 @@ async def _apply_client_affect(
*,
audit_hook: Optional[LlmAuditHook],
) -> None:
"""활성 Jev 평가를 1회 적용하고 생성 요청 직전 L3를 갱신한다."""
"""활성 Jev 평가(v2)를 1회 적용하고 생성 요청 직전 L3를 갱신한다.
① 이번 턴 반응 ② 기분 비대칭 전이 ③ 표현 계획(개방도 게이트)을 조합해
trace v2·속마음 요약·생성 지시 v2를 만든다(§6~§8.1). 속마음은 이 패킷에서
아직 저장·노출하지 않고 ctx에만 보존한다.
"""
if settings.client_affect_provider != "jev":
return
assert ctx.state_after is not None
state = client_affect.build_appraisal_state(
affect_baseline=ctx.persona.affect_baseline,
client_profile=_client_profile_inputs(ctx.persona),
affect_state=ctx.state_after.affect_state,
persona_context=_minimal_persona_context(ctx.persona),
affect_baseline=ctx.persona.affect_baseline,
stage=ctx.state_after.stage.value,
resistance=ctx.state_after.resistance,
effective_openness=ctx.state_after.effective_openness,
counselor_utterance=ctx.learner_text_masked,
@ -382,19 +397,23 @@ async def _apply_client_affect(
)
appraisal = await jev_client.appraise(state)
state_before_transition = ctx.state_after
transition = client_affect.transition_emotions(
transition = client_affect.transition_mood(
state_before_transition.affect_state,
ctx.persona.affect_baseline,
appraisal,
min_confidence=settings.jev_min_confidence,
)
ctx.state_after = replace(ctx.state_after, affect_state=transition.affect_state)
ctx.client_affect_trace = client_affect.build_client_affect_trace(
expression = client_affect.build_expression_plan(
appraisal, effective_openness=ctx.state_after.effective_openness
)
ctx.client_affect_trace = client_affect.build_client_affect_trace_v2(
affect_state_before=state_before_transition.affect_state,
affect_baseline=ctx.persona.affect_baseline,
affect_state_after=ctx.state_after.affect_state,
appraisal=appraisal,
transition=transition,
expression=expression,
turn_seq=ctx.state_after.turn_seq,
stage=ctx.state_after.stage.value,
resistance=ctx.state_after.resistance,
@ -402,6 +421,15 @@ async def _apply_client_affect(
rapport_credit=ctx.state_after.rapport_credit,
min_confidence=settings.jev_min_confidence,
)
ctx.client_inner_reaction = client_affect.build_inner_reaction(
appraisal, expression, turn_seq=ctx.state_after.turn_seq
)
ctx.client_affect_directive = client_affect.render_affect_directive_v2(
appraisal,
expression,
affect_state_after=ctx.state_after.affect_state,
affect_baseline=ctx.persona.affect_baseline,
)
ctx.client_affect_metadata = {
"provider": appraisal.provider,
"model": appraisal.model,