위기 신호 기술 검증을 보강
This commit is contained in:
parent
6988280b30
commit
35a9d8802c
5 changed files with 1164 additions and 14 deletions
|
|
@ -15,6 +15,7 @@ from . import session_persistence, turn_runtime
|
|||
from .contracts.engine_gateway import EngineGatewaySseLineDecoder
|
||||
from .deps import Principal, Role
|
||||
from .engine_client import EngineError, GenerateResponse
|
||||
from .paths import repo_path
|
||||
from .routes import eval as eval_routes
|
||||
from .routes import sessions
|
||||
from .routes import voice as voice_routes
|
||||
|
|
@ -80,6 +81,21 @@ def _session(principal: Principal) -> InProcSession:
|
|||
return sess
|
||||
|
||||
|
||||
def _crisis_case_signal(case_id: str) -> str:
|
||||
case_set = json.loads(
|
||||
repo_path(
|
||||
"data",
|
||||
"clinical",
|
||||
"p1-crisis-review-cases.json",
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
return next(
|
||||
case["synthetic_scenario"]["signal"]
|
||||
for case in case_set["cases"]
|
||||
if case["case_id"] == case_id
|
||||
)
|
||||
|
||||
|
||||
async def _consume_event_source(response: object) -> bytes:
|
||||
body = bytearray()
|
||||
iterator = getattr(response, "body_iterator")
|
||||
|
|
@ -726,7 +742,7 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
with patch.object(sessions.engine_client, "generate", should_not_generate):
|
||||
response = await sessions.submit_turn(
|
||||
sess.session_id,
|
||||
sessions.TurnRequest(text="저 지금 죽고싶어요. 도와주세요."),
|
||||
sessions.TurnRequest(text=_crisis_case_signal("P1-CRISIS-004")),
|
||||
principal,
|
||||
)
|
||||
|
||||
|
|
@ -918,7 +934,7 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
):
|
||||
response = await sessions.stream_turn(
|
||||
sess.session_id,
|
||||
sessions.TurnRequest(text="저 지금 자살하고 싶어요. 도와주세요."),
|
||||
sessions.TurnRequest(text=_crisis_case_signal("P1-CRISIS-004")),
|
||||
principal,
|
||||
)
|
||||
body = await _consume_event_source(response)
|
||||
|
|
@ -1446,6 +1462,113 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertTrue(official.citation)
|
||||
engine.generate.assert_called_once()
|
||||
|
||||
async def test_exact_persona_crisis_cases_fail_closed_to_risk_coaching(self) -> None:
|
||||
expected_locators = {
|
||||
"P1-CRISIS-001": "사고·계획·의도·행동 구분",
|
||||
"P1-CRISIS-002": "사고·계획·의도·행동 구분",
|
||||
"P1-CRISIS-005": "대한민국 109·119 긴급 연결",
|
||||
"P1-CRISIS-006": "자해 이후 심리사회적 평가",
|
||||
}
|
||||
|
||||
for case_id, expected_locator in expected_locators.items():
|
||||
with self.subTest(case_id=case_id):
|
||||
item = live_coach.LiveCoachInput(
|
||||
session_id=f"{case_id.lower()}-coach",
|
||||
turn_seq=4,
|
||||
stage="exploration",
|
||||
effective_openness=0.45,
|
||||
theory_mode="humanistic",
|
||||
persona_code="P1",
|
||||
persona_name="서연",
|
||||
learner_text="그 마음을 조금 더 들려줄 수 있을까요?",
|
||||
client_reply=_crisis_case_signal(case_id),
|
||||
recent_turns=[],
|
||||
)
|
||||
engine = SimpleNamespace(
|
||||
generate=AsyncMock(side_effect=AssertionError("crisis coaching must bypass engine"))
|
||||
)
|
||||
|
||||
suggestion = await live_coach.generate_live_coaching(item, engine=engine)
|
||||
|
||||
self.assertEqual(suggestion.status, "ready")
|
||||
self.assertEqual(suggestion.focus, "risk")
|
||||
self.assertEqual(suggestion.tone, "warn")
|
||||
self.assertIsNotNone(suggestion.safety_note)
|
||||
self.assertIn("가상내담자 위기 신호", suggestion.safety_note or "")
|
||||
self.assertNotEqual(suggestion.safety_note, guardrail.CRISIS_RESOURCE_MESSAGE)
|
||||
self.assertIn(
|
||||
expected_locator,
|
||||
[source.locator for source in suggestion.sources],
|
||||
)
|
||||
engine.generate.assert_not_called()
|
||||
|
||||
if case_id == "P1-CRISIS-005":
|
||||
self.assertIn("109", suggestion.message)
|
||||
self.assertIn("119", suggestion.message)
|
||||
self.assertIn("응급실", suggestion.message)
|
||||
self.assertIn("혼자 두지", suggestion.message)
|
||||
if case_id == "P1-CRISIS-002":
|
||||
for required in (
|
||||
"최근성",
|
||||
"빈도",
|
||||
"강도",
|
||||
"계획 유무",
|
||||
"의도",
|
||||
"접근 가능성",
|
||||
"지지자",
|
||||
"살아갈 이유",
|
||||
"도움 요청 가능성",
|
||||
):
|
||||
self.assertIn(required, suggestion.message)
|
||||
if case_id == "P1-CRISIS-006":
|
||||
self.assertIn("비판단", suggestion.message)
|
||||
self.assertIn("심리사회적", suggestion.message)
|
||||
self.assertIn("구체적인 수단", suggestion.message)
|
||||
self.assertIn("전문기관", suggestion.message)
|
||||
self.assertIn("추후 확인", suggestion.message)
|
||||
|
||||
async def test_live_coach_does_not_short_circuit_nonclinical_keyword_contexts(self) -> None:
|
||||
client_replies = (
|
||||
"이번 발표에 목숨 걸 만큼 열심히 했어요.",
|
||||
"자살 예방 교육에서 109를 배웠어요.",
|
||||
"게임에서 캐릭터 목숨이 하나 남았어요.",
|
||||
"저는 지금 자살하고 싶지 않아요.",
|
||||
"요즘은 죽고 싶지 않아요.",
|
||||
"저는 지금 죽고 싶은 건 아니에요.",
|
||||
"“저는 지금 죽고 싶어요” 문장을 번역해 주세요.",
|
||||
"저는 지금 죽고 싶은 생각이 없어요.",
|
||||
"저는 지금 죽고 싶은 마음이 전혀 없습니다.",
|
||||
"제가 지금 죽고 싶다고 말한 학생을 돕고 있어요.",
|
||||
"제가 지금 죽고 싶다고 한 친구를 돕고 있어요.",
|
||||
"저는 지금 자살 생각이 없어요.",
|
||||
"저는 지금 자해 충동이 없어요.",
|
||||
"저는 예전에 죽고 싶었지만 지금은 괜찮아요.",
|
||||
"제가 지금 여쭤보는 건 자살 생각이 있는지예요.",
|
||||
"제가 지금 묻는 질문은 혹시 죽고 싶은 생각이 있는지예요.",
|
||||
"누구나 지금 자살하고 싶다고 말하는 것은 아니에요.",
|
||||
)
|
||||
|
||||
for index, client_reply in enumerate(client_replies, start=1):
|
||||
with self.subTest(client_reply=client_reply):
|
||||
item = live_coach.LiveCoachInput(
|
||||
session_id=f"nonclinical-keyword-{index}",
|
||||
turn_seq=2,
|
||||
stage="exploration",
|
||||
effective_openness=0.5,
|
||||
theory_mode="humanistic",
|
||||
persona_code="P1",
|
||||
persona_name="서연",
|
||||
learner_text="그 경험을 조금 더 들려줄 수 있을까요?",
|
||||
client_reply=client_reply,
|
||||
recent_turns=[],
|
||||
)
|
||||
engine = SimpleNamespace(generate=AsyncMock(side_effect=EngineError("offline")))
|
||||
|
||||
suggestion = await live_coach.generate_live_coaching(item, engine=engine)
|
||||
|
||||
engine.generate.assert_awaited_once()
|
||||
self.assertNotEqual(suggestion.focus, "risk")
|
||||
|
||||
async def test_live_coach_prompt_uses_client_role_token_not_persona_name(
|
||||
self,
|
||||
) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue