Jev 내담자 평가·표현 v2와 속마음 공개
상담자 발화 판정(A)·감정(B)·표현(C) 20문항 질문 세트, 감쇠 없는 이번 턴 반응과 비대칭 기분 전이, 개방도 게이트로 생성 지시를 만들고 ccd.coping_strategy 전달 누락을 고친다. trace v2와 고정 문구 속마음 요약(migration 24, AI 경로 차단 RLS)을 같은 트랜잭션에 저장하고 피드백 정책이 켜진 경우에만 done·TurnResponse·음성 reply·리뷰로 노출한다. 회기 화면 속마음 보기 토글, 리뷰 접힘 블록, 관리자 감정 관측 v2 표시를 추가한다.
This commit is contained in:
parent
36cb847e38
commit
29c406d89f
39 changed files with 5726 additions and 343 deletions
|
|
@ -21,10 +21,64 @@ from .services import (
|
|||
rupture_scenario_director,
|
||||
state_machine,
|
||||
)
|
||||
from .services.jev_client import AppraisalResult, EMOTION_DIMENSIONS, EmotionEstimate, JevError
|
||||
from .services.jev_client import (
|
||||
AppraisalResult,
|
||||
ChoiceJudgment,
|
||||
EMOTION_DIMENSIONS,
|
||||
EmotionEstimate,
|
||||
JevError,
|
||||
NOUL_QUESTION_IDS,
|
||||
NoulJudgment,
|
||||
)
|
||||
from .store import InProcSession, store
|
||||
|
||||
|
||||
def _noul_judgments() -> dict[str, NoulJudgment]:
|
||||
# probability 0.3 → uncertain 미만 구간(false)에 가깝지만 게이트 테스트와 무관한 중립값.
|
||||
return {question_id: NoulJudgment(probability=0.3, confidence=0.6) for question_id in NOUL_QUESTION_IDS}
|
||||
|
||||
|
||||
def _one_hot(codes: list[str], picked: str) -> dict[str, float]:
|
||||
return {code: (1.0 if code == picked else 0.0) for code in codes}
|
||||
|
||||
|
||||
def _choice_judgments() -> dict[str, ChoiceJudgment]:
|
||||
a_coping_codes = ["nothing_asked", "manageable", "stretch", "overwhelming"]
|
||||
a_move_codes = [
|
||||
"reflection", "validation", "open_question", "closed_question", "clarification",
|
||||
"confrontation", "interpretation", "advice", "information", "self_disclosure",
|
||||
"topic_shift", "other",
|
||||
]
|
||||
c_behavior_codes = [
|
||||
"disclose_more", "stay_with_feeling", "hold_core", "ask_back", "minimal_response",
|
||||
"shift_topic", "abstract_talk", "appease", "self_blame", "complain", "argue_back",
|
||||
"take_control",
|
||||
]
|
||||
c_display_codes = ["as_felt", "softened", "covered_by_agreement", "masked"]
|
||||
return {
|
||||
"a_coping": ChoiceJudgment(
|
||||
choice="nothing_asked",
|
||||
probabilities=_one_hot(a_coping_codes, "nothing_asked"),
|
||||
confidence=0.6,
|
||||
),
|
||||
"a_move": ChoiceJudgment(
|
||||
choice="reflection",
|
||||
probabilities=_one_hot(a_move_codes, "reflection"),
|
||||
confidence=0.6,
|
||||
),
|
||||
"c_behavior": ChoiceJudgment(
|
||||
choice="disclose_more",
|
||||
probabilities=_one_hot(c_behavior_codes, "disclose_more"),
|
||||
confidence=0.6,
|
||||
),
|
||||
"c_display": ChoiceJudgment(
|
||||
choice="as_felt",
|
||||
probabilities=_one_hot(c_display_codes, "as_felt"),
|
||||
confidence=0.6,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _appraisal(
|
||||
*,
|
||||
score: float = 1.0,
|
||||
|
|
@ -42,6 +96,9 @@ def _appraisal(
|
|||
)
|
||||
for dimension in EMOTION_DIMENSIONS
|
||||
},
|
||||
noul_judgments=_noul_judgments(),
|
||||
choice_judgments=_choice_judgments(),
|
||||
sore_spot_count=0,
|
||||
model="jev-test",
|
||||
latency_ms=11,
|
||||
input_tokens=13,
|
||||
|
|
@ -360,9 +417,10 @@ class ClientAffectTransitionTest(unittest.TestCase):
|
|||
|
||||
def test_appraisal_state_re_masks_and_keeps_all_pinned_facts(self) -> None:
|
||||
state = client_affect.build_appraisal_state(
|
||||
affect_baseline={},
|
||||
client_profile={"presenting": "", "history": "", "core_belief": "서연은 가치가 없다고 느낀다."},
|
||||
affect_state={},
|
||||
persona_context={"core_belief": "서연은 가치가 없다고 느낀다."},
|
||||
affect_baseline={},
|
||||
stage="라포",
|
||||
resistance=0.5,
|
||||
effective_openness=0.3,
|
||||
counselor_utterance="김상담 연락처 010-1234-5678",
|
||||
|
|
@ -373,16 +431,28 @@ class ClientAffectTransitionTest(unittest.TestCase):
|
|||
client_identity="서연",
|
||||
)
|
||||
|
||||
self.assertEqual(set(state), {"persona", "memory", "recent_turns", "counselor_utterance", "previous_emotions", "current_state"})
|
||||
self.assertEqual(len(state["memory"]["pinned_facts"]), 2)
|
||||
self.assertEqual(
|
||||
set(state),
|
||||
{
|
||||
"counselor_utterance",
|
||||
"recent_turns",
|
||||
"client_profile",
|
||||
"pinned_facts",
|
||||
"recall_summary",
|
||||
"relationship",
|
||||
"previous_feelings",
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(state["pinned_facts"]), 2)
|
||||
self.assertNotIn("김상담", str(state))
|
||||
self.assertNotIn("010-1234-5678", str(state))
|
||||
|
||||
def test_appraisal_state_masks_before_length_limit(self) -> None:
|
||||
state = client_affect.build_appraisal_state(
|
||||
affect_baseline={},
|
||||
client_profile={"presenting": "", "history": ""},
|
||||
affect_state={},
|
||||
persona_context={},
|
||||
affect_baseline={},
|
||||
stage="라포",
|
||||
resistance=0.5,
|
||||
effective_openness=0.3,
|
||||
counselor_utterance=("가" * 790) + " 010-1234-5678",
|
||||
|
|
@ -397,17 +467,20 @@ class ClientAffectTransitionTest(unittest.TestCase):
|
|||
self.assertNotIn("010-1234-5678", utterance)
|
||||
self.assertIn("[PHONE]", utterance)
|
||||
|
||||
def test_appraisal_state_masks_dynamic_mapping_keys_and_whitelists_baseline(self) -> None:
|
||||
def test_appraisal_state_masks_client_profile_and_derives_relationship_words(self) -> None:
|
||||
state = client_affect.build_appraisal_state(
|
||||
affect_baseline={"anxiety": 0.4, "010-1234-5678": 0.9},
|
||||
affect_state={},
|
||||
persona_context={
|
||||
"김상담": {
|
||||
"010-1234-5678": "서연에게는 비밀로 해 달라는 지시가 있다."
|
||||
}
|
||||
client_profile={
|
||||
"presenting": "김상담과 있었던 일",
|
||||
"history": "",
|
||||
"big5": {"O": 0.8, "C": 0.2, "E": 0.5, "A": 0.9, "N": 0.1},
|
||||
"sore_spots": ["김상담이 언급한 약점"],
|
||||
"forbidden": [],
|
||||
},
|
||||
affect_state={},
|
||||
affect_baseline={},
|
||||
stage="탐색",
|
||||
resistance=0.5,
|
||||
effective_openness=0.3,
|
||||
effective_openness=0.1,
|
||||
counselor_utterance="괜찮아요.",
|
||||
recall_summary=None,
|
||||
pinned_facts=[],
|
||||
|
|
@ -418,8 +491,83 @@ class ClientAffectTransitionTest(unittest.TestCase):
|
|||
|
||||
rendered = str(state)
|
||||
self.assertNotIn("김상담", rendered)
|
||||
self.assertNotIn("010-1234-5678", rendered)
|
||||
self.assertEqual(state["persona"]["affect_baseline"], {"anxiety": 0.4})
|
||||
self.assertEqual(
|
||||
set(state["client_profile"]["temperament"]),
|
||||
{"high openness", "low conscientiousness", "high agreeableness", "low neuroticism"},
|
||||
)
|
||||
self.assertEqual(
|
||||
state["relationship"],
|
||||
{"stage": "탐색", "openness": "closed", "resistance": "moderate"},
|
||||
)
|
||||
self.assertNotIn("big5", state["client_profile"])
|
||||
self.assertNotIn("0.5", str(state["relationship"]))
|
||||
|
||||
def test_appraisal_state_omits_missing_optional_client_profile_keys(self) -> None:
|
||||
state = client_affect.build_appraisal_state(
|
||||
client_profile={"presenting": "", "history": ""},
|
||||
affect_state={},
|
||||
affect_baseline={},
|
||||
stage="라포",
|
||||
resistance=0.1,
|
||||
effective_openness=0.1,
|
||||
counselor_utterance="",
|
||||
recall_summary=None,
|
||||
pinned_facts=[],
|
||||
recent_turns=[],
|
||||
counselor_identity=None,
|
||||
client_identity=None,
|
||||
)
|
||||
|
||||
self.assertNotIn("core_belief", state["client_profile"])
|
||||
self.assertNotIn("automatic_thought", state["client_profile"])
|
||||
self.assertNotIn("coping_strategy", state["client_profile"])
|
||||
self.assertNotIn("temperament", state["client_profile"])
|
||||
self.assertNotIn("speech_style", state["client_profile"])
|
||||
self.assertEqual(state["client_profile"]["sore_spots"], [])
|
||||
self.assertEqual(state["client_profile"]["forbidden"], [])
|
||||
|
||||
def test_appraisal_state_reads_coping_strategy_key_not_legacy_coping_key(self) -> None:
|
||||
state = client_affect.build_appraisal_state(
|
||||
client_profile={
|
||||
"presenting": "",
|
||||
"history": "",
|
||||
"coping_strategy": "거리를 두고 관찰한다.",
|
||||
},
|
||||
affect_state={},
|
||||
affect_baseline={},
|
||||
stage="라포",
|
||||
resistance=0.1,
|
||||
effective_openness=0.1,
|
||||
counselor_utterance="",
|
||||
recall_summary=None,
|
||||
pinned_facts=[],
|
||||
recent_turns=[],
|
||||
counselor_identity=None,
|
||||
client_identity=None,
|
||||
)
|
||||
|
||||
self.assertEqual(state["client_profile"]["coping_strategy"], "거리를 두고 관찰한다.")
|
||||
|
||||
def test_appraisal_state_previous_feelings_use_word_buckets_not_numbers(self) -> None:
|
||||
state = client_affect.build_appraisal_state(
|
||||
client_profile={"presenting": "", "history": ""},
|
||||
affect_state={"emotion_anxiety": 0.05, "emotion_trust": 0.95},
|
||||
affect_baseline={},
|
||||
stage="라포",
|
||||
resistance=0.1,
|
||||
effective_openness=0.1,
|
||||
counselor_utterance="",
|
||||
recall_summary=None,
|
||||
pinned_facts=[],
|
||||
recent_turns=[],
|
||||
counselor_identity=None,
|
||||
client_identity=None,
|
||||
)
|
||||
|
||||
self.assertEqual(state["previous_feelings"]["anxiety"], "absent")
|
||||
self.assertEqual(state["previous_feelings"]["trust"], "overwhelming")
|
||||
self.assertNotIn("0.05", str(state))
|
||||
self.assertNotIn("0.95", str(state))
|
||||
|
||||
|
||||
class ClientAffectRuntimeTest(unittest.IsolatedAsyncioTestCase):
|
||||
|
|
@ -709,3 +857,291 @@ class ClientAffectRuntimeTest(unittest.IsolatedAsyncioTestCase):
|
|||
|
||||
self.assertEqual(carry.end_state["affect"]["emotion_anxiety"], 0.6)
|
||||
self.assertEqual(response.end_state["affect"], {"negative_affect": 0.7})
|
||||
|
||||
|
||||
def _judged_noul(probability: float, confidence: float | None = 0.7) -> NoulJudgment:
|
||||
return NoulJudgment(probability=probability, confidence=confidence)
|
||||
|
||||
|
||||
def _judged_choice(
|
||||
codes: list[str], choice: str, *, top_probability: float = 0.7, confidence: float | None = 0.6
|
||||
) -> ChoiceJudgment:
|
||||
rest = (1.0 - top_probability) / (len(codes) - 1) if len(codes) > 1 else 0.0
|
||||
return ChoiceJudgment(
|
||||
choice=choice,
|
||||
probabilities={code: (top_probability if code == choice else rest) for code in codes},
|
||||
confidence=confidence,
|
||||
)
|
||||
|
||||
|
||||
_A_CODING_CODES = [
|
||||
"reflection", "validation", "open_question", "closed_question", "clarification",
|
||||
"confrontation", "interpretation", "advice", "information", "self_disclosure",
|
||||
"topic_shift", "other",
|
||||
]
|
||||
_COPING_CODES = ["nothing_asked", "manageable", "stretch", "overwhelming"]
|
||||
_BEHAVIOR_CODES = [
|
||||
"disclose_more", "stay_with_feeling", "hold_core", "ask_back", "minimal_response",
|
||||
"shift_topic", "abstract_talk", "appease", "self_blame", "complain", "argue_back",
|
||||
"take_control",
|
||||
]
|
||||
_DISPLAY_CODES = ["as_felt", "softened", "covered_by_agreement", "masked"]
|
||||
|
||||
|
||||
def _v2_appraisal(
|
||||
*,
|
||||
noul_overrides: dict[str, NoulJudgment] | None = None,
|
||||
choice_overrides: dict[str, ChoiceJudgment] | None = None,
|
||||
emotion_score: float = 0.5,
|
||||
emotion_confidence: float | None = 0.9,
|
||||
sore_spot_count: int = 0,
|
||||
) -> AppraisalResult:
|
||||
nouls = {question_id: _judged_noul(0.5) for question_id in NOUL_QUESTION_IDS}
|
||||
nouls.update(noul_overrides or {})
|
||||
choices = {
|
||||
"a_coping": _judged_choice(_COPING_CODES, "nothing_asked"),
|
||||
"a_move": _judged_choice(_A_CODING_CODES, "reflection"),
|
||||
"c_behavior": _judged_choice(_BEHAVIOR_CODES, "disclose_more"),
|
||||
"c_display": _judged_choice(_DISPLAY_CODES, "as_felt"),
|
||||
}
|
||||
choices.update(choice_overrides or {})
|
||||
return AppraisalResult(
|
||||
emotions={
|
||||
dimension: EmotionEstimate(score=emotion_score, confidence=emotion_confidence)
|
||||
for dimension in EMOTION_DIMENSIONS
|
||||
},
|
||||
noul_judgments=nouls,
|
||||
choice_judgments=choices,
|
||||
sore_spot_count=sore_spot_count,
|
||||
model="jev-test",
|
||||
latency_ms=5,
|
||||
input_tokens=1,
|
||||
output_tokens=1,
|
||||
provider="typesafe",
|
||||
cost_usd=None,
|
||||
)
|
||||
|
||||
|
||||
class ClientAffectV2CompositionTest(unittest.TestCase):
|
||||
def test_interpret_noul_thresholds(self) -> None:
|
||||
self.assertIs(client_affect.interpret_noul(NoulJudgment(probability=0.6)), True)
|
||||
self.assertIs(client_affect.interpret_noul(NoulJudgment(probability=0.4)), False)
|
||||
self.assertEqual(client_affect.interpret_noul(NoulJudgment(probability=0.5)), "uncertain")
|
||||
self.assertIsNone(client_affect.interpret_noul(None))
|
||||
|
||||
def test_interpret_choice_threshold(self) -> None:
|
||||
decided = ChoiceJudgment(choice="a", probabilities={"a": 0.45, "b": 0.55})
|
||||
self.assertEqual(client_affect.interpret_choice(decided), "b")
|
||||
undecided = ChoiceJudgment(choice="a", probabilities={"a": 0.34, "b": 0.33, "c": 0.33})
|
||||
self.assertEqual(client_affect.interpret_choice(undecided), "uncertain")
|
||||
self.assertIsNone(client_affect.interpret_choice(None))
|
||||
|
||||
def test_build_reaction_requires_confidence_at_least_035_and_uses_raw_score(self) -> None:
|
||||
appraisal = _v2_appraisal(emotion_score=0.6, emotion_confidence=0.35)
|
||||
included = client_affect.build_reaction(appraisal)
|
||||
self.assertEqual(set(included), set(EMOTION_DIMENSIONS))
|
||||
self.assertEqual(included["anxiety"], 0.6)
|
||||
|
||||
excluded = _v2_appraisal(emotion_score=0.6, emotion_confidence=0.34)
|
||||
self.assertEqual(client_affect.build_reaction(excluded), {})
|
||||
|
||||
def test_openness_gate_three_zones(self) -> None:
|
||||
closed = client_affect.build_expression_plan(
|
||||
_v2_appraisal(
|
||||
choice_overrides={"c_behavior": _judged_choice(_BEHAVIOR_CODES, "disclose_more")}
|
||||
),
|
||||
effective_openness=0.1,
|
||||
)
|
||||
self.assertEqual(closed.gated_behavior, "minimal_response")
|
||||
self.assertEqual(closed.gate_reason, "openness_closed")
|
||||
|
||||
guarded = client_affect.build_expression_plan(
|
||||
_v2_appraisal(
|
||||
choice_overrides={"c_behavior": _judged_choice(_BEHAVIOR_CODES, "disclose_more")}
|
||||
),
|
||||
effective_openness=0.3,
|
||||
)
|
||||
self.assertEqual(guarded.gated_behavior, "hold_core")
|
||||
self.assertEqual(guarded.gate_reason, "openness_guarded")
|
||||
|
||||
open_zone = client_affect.build_expression_plan(
|
||||
_v2_appraisal(
|
||||
choice_overrides={"c_behavior": _judged_choice(_BEHAVIOR_CODES, "disclose_more")}
|
||||
),
|
||||
effective_openness=0.5,
|
||||
)
|
||||
self.assertEqual(open_zone.gated_behavior, "disclose_more")
|
||||
self.assertIsNone(open_zone.gate_reason)
|
||||
|
||||
def test_stance_derives_from_gated_behavior(self) -> None:
|
||||
plan = client_affect.build_expression_plan(
|
||||
_v2_appraisal(
|
||||
choice_overrides={"c_behavior": _judged_choice(_BEHAVIOR_CODES, "complain")}
|
||||
),
|
||||
effective_openness=0.9,
|
||||
)
|
||||
self.assertEqual(plan.stance, "push_back")
|
||||
|
||||
def test_hidden_gap_requires_masking_display_and_strong_negative_reaction(self) -> None:
|
||||
masked_and_strong = client_affect.build_expression_plan(
|
||||
_v2_appraisal(
|
||||
choice_overrides={"c_display": _judged_choice(_DISPLAY_CODES, "masked")},
|
||||
emotion_score=0.6,
|
||||
emotion_confidence=0.9,
|
||||
),
|
||||
effective_openness=0.9,
|
||||
)
|
||||
self.assertTrue(masked_and_strong.hidden_gap)
|
||||
|
||||
as_felt_and_strong = client_affect.build_expression_plan(
|
||||
_v2_appraisal(emotion_score=0.6, emotion_confidence=0.9),
|
||||
effective_openness=0.9,
|
||||
)
|
||||
self.assertFalse(as_felt_and_strong.hidden_gap)
|
||||
|
||||
masked_but_weak = client_affect.build_expression_plan(
|
||||
_v2_appraisal(
|
||||
choice_overrides={"c_display": _judged_choice(_DISPLAY_CODES, "masked")},
|
||||
emotion_score=0.2,
|
||||
emotion_confidence=0.9,
|
||||
),
|
||||
effective_openness=0.9,
|
||||
)
|
||||
self.assertFalse(masked_but_weak.hidden_gap)
|
||||
|
||||
def test_experienced_phrases_priority_order_and_limit(self) -> None:
|
||||
appraisal = _v2_appraisal(
|
||||
noul_overrides={
|
||||
"a_fact_conflict": _judged_noul(0.9),
|
||||
"a_judged": _judged_noul(0.9),
|
||||
"a_autonomy": _judged_noul(0.9),
|
||||
},
|
||||
choice_overrides={
|
||||
"a_coping": _judged_choice(_COPING_CODES, "overwhelming"),
|
||||
},
|
||||
)
|
||||
top2 = client_affect.experienced_phrases(appraisal, limit=2)
|
||||
self.assertEqual(
|
||||
top2,
|
||||
[
|
||||
"자신의 사정과 다른 전제를 들었다고 느꼈다",
|
||||
"평가받거나 탓을 듣는 것처럼 느꼈다",
|
||||
],
|
||||
)
|
||||
top3 = client_affect.experienced_phrases(appraisal, limit=3)
|
||||
self.assertEqual(len(top3), 3)
|
||||
self.assertEqual(top3[2], "무엇을 할지 정해 주는 것 같아 압박을 느꼈다")
|
||||
|
||||
def test_experienced_phrases_empty_when_all_uncertain_or_false(self) -> None:
|
||||
appraisal = _v2_appraisal() # 모든 noul probability=0.3 → false, choice는 experience에 안 걸림
|
||||
self.assertEqual(client_affect.experienced_phrases(appraisal, limit=2), [])
|
||||
|
||||
def _assert_no_numbers_english_codes_or_leak_markers(self, directive: str) -> None:
|
||||
self.assertIn("정서 연기 지시:", directive)
|
||||
self.assertNotIn("내부 상태", directive)
|
||||
for forbidden_code in (
|
||||
"disclose_more", "hold_core", "as_felt", "withdrawal", "confrontation", "RUPTURE_TYPES",
|
||||
):
|
||||
self.assertNotIn(forbidden_code, directive)
|
||||
# 고정 문구("1~3문장")를 제외하면 확률·점수 같은 소수 숫자가 없어야 한다.
|
||||
self.assertNotRegex(directive.replace("1~3문장", ""), r"\d")
|
||||
|
||||
def test_render_affect_directive_v2_has_no_numbers_english_codes_or_leak_markers(self) -> None:
|
||||
appraisal = _v2_appraisal(
|
||||
noul_overrides={"a_judged": _judged_noul(0.9)},
|
||||
emotion_score=0.8,
|
||||
emotion_confidence=0.9,
|
||||
)
|
||||
expression = client_affect.build_expression_plan(appraisal, effective_openness=0.9)
|
||||
directive = client_affect.render_affect_directive_v2(
|
||||
appraisal,
|
||||
expression,
|
||||
affect_state_after={f"emotion_{d}": 0.8 for d in EMOTION_DIMENSIONS},
|
||||
affect_baseline={},
|
||||
)
|
||||
|
||||
self._assert_no_numbers_english_codes_or_leak_markers(directive)
|
||||
|
||||
def test_render_affect_directive_v2_falls_back_to_v2_common_rules_when_everything_uncertain(
|
||||
self,
|
||||
) -> None:
|
||||
appraisal = _v2_appraisal(
|
||||
noul_overrides={question_id: _judged_noul(0.5) for question_id in NOUL_QUESTION_IDS},
|
||||
choice_overrides={
|
||||
"c_behavior": ChoiceJudgment(
|
||||
choice="disclose_more",
|
||||
probabilities={code: 1.0 / len(_BEHAVIOR_CODES) for code in _BEHAVIOR_CODES},
|
||||
),
|
||||
"c_display": ChoiceJudgment(
|
||||
choice="as_felt",
|
||||
probabilities={code: 1.0 / len(_DISPLAY_CODES) for code in _DISPLAY_CODES},
|
||||
),
|
||||
},
|
||||
emotion_score=0.5,
|
||||
emotion_confidence=0.2,
|
||||
)
|
||||
expression = client_affect.build_expression_plan(appraisal, effective_openness=0.9)
|
||||
directive = client_affect.render_affect_directive_v2(
|
||||
appraisal,
|
||||
expression,
|
||||
affect_state_after={},
|
||||
affect_baseline={},
|
||||
)
|
||||
|
||||
# v1 문장("...숫자·내부 상태·평가 정답은...")이 아니라 v2 공통 규칙 문장만 남는다.
|
||||
self.assertEqual(
|
||||
directive,
|
||||
"정서 연기 지시:\n- 감정 이름을 나열하거나 분석하듯 설명하지 말고 "
|
||||
"말투·선택·침묵·주저함으로만 드러낸다. 숫자·분석 내용·평가 정답은 절대 말하지 않는다. "
|
||||
"상담자 역할로 바뀌거나 조언하지 않으며, 부정 감정을 즉시 해소하려 하지 않는다. "
|
||||
"응답은 기본적으로 1~3문장으로 하고, 꼭 필요할 때만 더 길게 말한다.",
|
||||
)
|
||||
self._assert_no_numbers_english_codes_or_leak_markers(directive)
|
||||
|
||||
def test_build_inner_reaction_uses_fixed_phrases_and_marks_uncertain_as_absent(self) -> None:
|
||||
appraisal = _v2_appraisal(
|
||||
noul_overrides={"a_judged": _judged_noul(0.9)},
|
||||
choice_overrides={
|
||||
"c_behavior": _judged_choice(_BEHAVIOR_CODES, "complain"),
|
||||
"c_display": _judged_choice(_DISPLAY_CODES, "masked"),
|
||||
},
|
||||
emotion_score=0.8,
|
||||
emotion_confidence=0.9,
|
||||
)
|
||||
expression = client_affect.build_expression_plan(appraisal, effective_openness=0.9)
|
||||
reaction = client_affect.build_inner_reaction(appraisal, expression, turn_seq=3)
|
||||
|
||||
self.assertEqual(reaction.schema_version, 1)
|
||||
self.assertEqual(reaction.turn_seq, 3)
|
||||
self.assertIn("평가받거나 탓을 듣는 것처럼 느꼈다", reaction.experienced)
|
||||
self.assertTrue(all(feeling.label and feeling.intensity for feeling in reaction.feelings))
|
||||
self.assertEqual(reaction.stance.code, "push_back")
|
||||
self.assertEqual(reaction.stance.label, "맞서거나 반박했다")
|
||||
self.assertEqual(reaction.display.code, "masked")
|
||||
self.assertTrue(reaction.hidden_gap)
|
||||
|
||||
uncertain_appraisal = _v2_appraisal(
|
||||
noul_overrides={question_id: _judged_noul(0.5) for question_id in NOUL_QUESTION_IDS},
|
||||
choice_overrides={
|
||||
"c_behavior": ChoiceJudgment(
|
||||
choice="disclose_more",
|
||||
probabilities={code: 1.0 / len(_BEHAVIOR_CODES) for code in _BEHAVIOR_CODES},
|
||||
),
|
||||
"c_display": ChoiceJudgment(
|
||||
choice="as_felt",
|
||||
probabilities={code: 1.0 / len(_DISPLAY_CODES) for code in _DISPLAY_CODES},
|
||||
),
|
||||
},
|
||||
emotion_confidence=0.2,
|
||||
)
|
||||
uncertain_expression = client_affect.build_expression_plan(
|
||||
uncertain_appraisal, effective_openness=0.9
|
||||
)
|
||||
uncertain_reaction = client_affect.build_inner_reaction(
|
||||
uncertain_appraisal, uncertain_expression, turn_seq=1
|
||||
)
|
||||
self.assertEqual(uncertain_reaction.experienced, ())
|
||||
self.assertEqual(uncertain_reaction.feelings, ())
|
||||
self.assertIsNone(uncertain_reaction.stance)
|
||||
self.assertIsNone(uncertain_reaction.display)
|
||||
self.assertFalse(uncertain_reaction.hidden_gap)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue