위기 표현 경계를 확장
This commit is contained in:
parent
35a9d8802c
commit
aeca42475b
3 changed files with 115 additions and 7 deletions
|
|
@ -469,8 +469,10 @@ _CRISIS_TERMS = [
|
|||
"죽고 싶", "죽고싶", "자살 생각", "자살하고 싶", "자살을 생각", "자살 충동",
|
||||
"자살 시도", "목숨을 끊", "목숨을 버리", "사라지고 싶", "없어지고 싶",
|
||||
"자해하고 싶", "자해한 적", "자해를 했", "자해 충동", "자해 시도",
|
||||
"스스로를 다치게", "끝내고 싶", "살기 싫",
|
||||
"살아서 뭐", "죽어야", "안전을 스스로 지키기 어렵",
|
||||
"자살 계획", "자해 계획", "자살하려고", "자해하려고", "죽으려고",
|
||||
"스스로를 다치게", "스스로를 해칠 계획", "자살할지도", "끝내고 싶",
|
||||
"살기 싫", "살아서 뭐", "죽어야", "안전을 스스로 지키기 어렵",
|
||||
"안전을 유지할 수 없",
|
||||
]
|
||||
# 자살·자해 단어를 직접 쓰지 않더라도 본인의 현재 위험과 실제 도움 필요를 함께
|
||||
# 밝히는 짧은 진술은 엔진 호출 전에 막아야 한다. 단일 '위험'/'도움' 키워드만으로는
|
||||
|
|
@ -503,7 +505,7 @@ _PERSONAL_DANGER_PATTERNS = (
|
|||
),
|
||||
)
|
||||
_PERSONAL_IMMEDIATE_HARM = (
|
||||
r"(?:안전하지\s*않|안전을?\s*지킬\s*수\s*없|"
|
||||
r"(?:안전하지\s*않|안전을?\s*(?:지킬|유지할)\s*수\s*없|"
|
||||
r"(?:제|내)\s*안전을?\s*지킬\s*(?:자신이|수)?\s*없|"
|
||||
r"혼자\s*있으면\s*위험|"
|
||||
r"(?:스스로를|저를|나를|제\s*자신을|내\s*자신을)\s*"
|
||||
|
|
@ -527,11 +529,14 @@ _PERSONAL_IMMEDIATE_HARM_PATTERNS = (
|
|||
),
|
||||
)
|
||||
_CURRENT_SUICIDE_OR_SELF_HARM = (
|
||||
r"(?:자살(?:하고)?\s*싶|자살할\s*것\s*같|죽고\s*싶|"
|
||||
r"(?:자살(?:하고)?\s*싶|자살(?:하)?려고\s*(?:해요|합니다)|"
|
||||
r"자살할\s*(?:것\s*같|지도\s*모르)|죽고\s*싶|"
|
||||
r"죽으려고\s*(?:해요|합니다)|"
|
||||
r"죽어버리고\s*싶|살기\s*싫|살고\s*싶지\s*않|"
|
||||
r"자해(?:하고)?\s*싶|자해할\s*것\s*같|"
|
||||
r"자해(?:하고)?\s*싶|자해(?:하)?려고\s*(?:해요|합니다)|"
|
||||
r"자해할\s*(?:것\s*같|지도\s*모르)|"
|
||||
r"(?:스스로를|저를|나를)\s*(?:다치게\s*하고|해치고)\s*싶|"
|
||||
r"(?:제|내)\s*목숨을\s*끊고\s*싶|삶을\s*끝내고\s*싶|"
|
||||
r"(?:제|내)\s*목숨을\s*(?:끊고\s*싶|끊으려고\s*(?:해요|합니다))|"
|
||||
r"끝내고\s*싶|없어지고\s*싶|죽어야\s*할\s*것\s*같|"
|
||||
r"살아갈\s*이유가\s*없|자해\s*충동이\s*있)"
|
||||
)
|
||||
|
|
@ -548,6 +553,12 @@ _FIRST_PERSON_CURRENT_CRISIS_PATTERNS = (
|
|||
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*(?:정말|진짜)\s*"
|
||||
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||
),
|
||||
re.compile(
|
||||
rf"{_CURRENT}\s*(?:당장\s*)?"
|
||||
r"(?:자살(?:하)?려고\s*(?:해요|합니다)|"
|
||||
r"죽으려고\s*(?:해요|합니다)|"
|
||||
r"자해(?:하)?려고\s*(?:해요|합니다))"
|
||||
),
|
||||
)
|
||||
_FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS = (
|
||||
re.compile(
|
||||
|
|
@ -558,12 +569,27 @@ _FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS = (
|
|||
rf"당장\s*{_SELF_SUBJECT}(?:이|은|도)?\s*"
|
||||
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||
),
|
||||
re.compile(
|
||||
r"(?<![가-힣A-Za-z0-9])(?:제|내)\s*목숨을\s*"
|
||||
r"(?:끊고\s*싶|끊으려고\s*(?:해요|합니다))"
|
||||
),
|
||||
)
|
||||
_REAL_HELP_CUE = re.compile(
|
||||
r"(?:(?:실제로\s*)?도움(?:이|을)?\s*필요|도와(?:주|줄|주세요))"
|
||||
)
|
||||
_CURRENT_PERSONAL_DANGER_HELP = "first_person_current_danger_help"
|
||||
_FIRST_PERSON_CURRENT_CRISIS = "first_person_current_suicide_or_self_harm"
|
||||
_EXPLICIT_HIGH_RISK = "explicit_suicide_or_self_harm_risk"
|
||||
_EXPLICIT_HIGH_RISK_CUE = re.compile(
|
||||
r"(?<![가-힣A-Za-z0-9])(?:"
|
||||
r"(?:자살|자해)\s*(?:계획|충동|생각)(?:이|가|은|는)?\s*(?:있|세웠)|"
|
||||
r"(?:자살|자해)\s*시도(?:를|가|는)?\s*(?:하)?려고\s*(?:해요|합니다)|"
|
||||
r"(?:스스로를|저를|나를|제\s*자신을|내\s*자신을)\s*해칠\s*계획"
|
||||
r"(?:이|가|은|는)?\s*(?:있|세웠)|"
|
||||
r"(?:자살|자해)할지도\s*모르|"
|
||||
r"(?:제|내)\s*안전을?\s*(?:지킬|유지할)\s*수\s*없"
|
||||
r")"
|
||||
)
|
||||
_INDIRECT_SELF_ERASURE = "indirect_self_erasure"
|
||||
_INDIRECT_SELF_ERASURE_CUE = re.compile(
|
||||
r"(?<![가-힣A-Za-z0-9])(?:차라리\s*(?:없어졌으면|사라졌으면)|"
|
||||
|
|
@ -607,6 +633,12 @@ _NEGATED_CRISIS = re.compile(
|
|||
r"(?:자살(?:하고)?|죽고|자해(?:하고)?)\s*싶은\s*(?:생각|마음)"
|
||||
r"(?:이|은)?\s*(?:전혀\s*)?(?:없|들지\s*않)|"
|
||||
r"(?:자살\s*생각|자해\s*충동)(?:이|은)?\s*(?:전혀\s*)?(?:없|들지\s*않)|"
|
||||
r"(?:자살|자해)할\s*것\s*같지\s*않|살기\s*싫지\s*않|"
|
||||
r"없어지고\s*싶지\s*않|죽어야\s*할\s*것\s*같지\s*않|"
|
||||
r"살아갈\s*이유가\s*없는\s*(?:건|것은)\s*아니|"
|
||||
r"자해\s*충동이\s*있지는\s*않|"
|
||||
r"(?:자살|자해)\s*(?:계획|충동|생각|시도)(?:이|가|은|는)?\s*"
|
||||
r"(?:전혀\s*)?(?:없|있지는\s*않|하지\s*않)|"
|
||||
r"위험(?:한|할)\s*것\s*같지\s*않|도움(?:이|을)?\s*필요하지\s*않"
|
||||
)
|
||||
_RECOVERED_PAST_CRISIS = re.compile(
|
||||
|
|
@ -716,6 +748,11 @@ def crisis_signal_matches(text: str) -> list[str]:
|
|||
matched.append(_CURRENT_PERSONAL_DANGER_HELP)
|
||||
if _first_person_current_crisis(text):
|
||||
matched.append(_FIRST_PERSON_CURRENT_CRISIS)
|
||||
if any(
|
||||
not _match_has_invalid_crisis_context(text, match)
|
||||
for match in _EXPLICIT_HIGH_RISK_CUE.finditer(text)
|
||||
):
|
||||
matched.append(_EXPLICIT_HIGH_RISK)
|
||||
return matched
|
||||
|
||||
|
||||
|
|
@ -739,8 +776,14 @@ def classify_crisis(text: str, *, speaker_is_persona_context: bool = True) -> Cr
|
|||
risk = min(5, 2 + len(matched)) # 거친 단계화(휴리스틱)
|
||||
explicit_real_signal = _CURRENT_PERSONAL_DANGER_HELP in matched
|
||||
explicit_current_crisis = _FIRST_PERSON_CURRENT_CRISIS in matched
|
||||
explicit_high_risk = _EXPLICIT_HIGH_RISK in matched
|
||||
|
||||
if not speaker_is_persona_context or explicit_real_signal or explicit_current_crisis:
|
||||
if (
|
||||
not speaker_is_persona_context
|
||||
or explicit_real_signal
|
||||
or explicit_current_crisis
|
||||
or explicit_high_risk
|
||||
):
|
||||
# 실제 위기 가능 → 에스컬레이션(보수적)
|
||||
return CrisisResult(
|
||||
kind=CrisisKind.LEARNER_REAL,
|
||||
|
|
|
|||
|
|
@ -254,6 +254,18 @@ class ClientReplyQualityGateTest(unittest.TestCase):
|
|||
"저는 지금 저를 해칠지도 몰라요. 도와주세요.",
|
||||
"제가 지금 저를 다치게 할까 봐 두려워요. 도움이 필요해요.",
|
||||
"저는 지금 혼자 있으면 위험해요. 도와주세요.",
|
||||
"저는 지금 자살하려고 해요.",
|
||||
"지금 당장 죽으려고 해요.",
|
||||
"제 목숨을 끊으려고 해요.",
|
||||
"자살 계획이 있어요.",
|
||||
"자해 계획이 있어요.",
|
||||
"자살 시도를 하려고 해요.",
|
||||
"자해 시도를 하려고 해요.",
|
||||
"자살 충동이 있어요.",
|
||||
"자살 생각이 있어요.",
|
||||
"자살할지도 모르겠어요.",
|
||||
"스스로를 해칠 계획이 있어요.",
|
||||
"제 안전을 유지할 수 없어요.",
|
||||
)
|
||||
for signal in real_help_questions:
|
||||
with self.subTest(signal=signal):
|
||||
|
|
@ -311,6 +323,13 @@ class ClientReplyQualityGateTest(unittest.TestCase):
|
|||
"저는 지금 자살 생각이 없어요.",
|
||||
"저는 지금 자해 충동이 없어요.",
|
||||
"저는 예전에 죽고 싶었지만 지금은 괜찮아요.",
|
||||
"저는 지금 자살할 것 같지 않아요.",
|
||||
"저는 지금 자해할 것 같지 않아요.",
|
||||
"저는 지금 살기 싫지 않아요.",
|
||||
"저는 지금 없어지고 싶지 않아요.",
|
||||
"저는 지금 죽어야 할 것 같지 않아요.",
|
||||
"저는 지금 살아갈 이유가 없는 건 아니에요.",
|
||||
"저는 지금 자해 충동이 있지는 않아요.",
|
||||
)
|
||||
self.assertFalse(nonclinical_first_person.escalate)
|
||||
self.assertEqual(ordinary_wish.kind, guardrail.CrisisKind.NONE)
|
||||
|
|
|
|||
|
|
@ -1546,6 +1546,13 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
"제가 지금 여쭤보는 건 자살 생각이 있는지예요.",
|
||||
"제가 지금 묻는 질문은 혹시 죽고 싶은 생각이 있는지예요.",
|
||||
"누구나 지금 자살하고 싶다고 말하는 것은 아니에요.",
|
||||
"저는 지금 자살할 것 같지 않아요.",
|
||||
"저는 지금 자해할 것 같지 않아요.",
|
||||
"저는 지금 살기 싫지 않아요.",
|
||||
"저는 지금 없어지고 싶지 않아요.",
|
||||
"저는 지금 죽어야 할 것 같지 않아요.",
|
||||
"저는 지금 살아갈 이유가 없는 건 아니에요.",
|
||||
"저는 지금 자해 충동이 있지는 않아요.",
|
||||
)
|
||||
|
||||
for index, client_reply in enumerate(client_replies, start=1):
|
||||
|
|
@ -1569,6 +1576,45 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
engine.generate.assert_awaited_once()
|
||||
self.assertNotEqual(suggestion.focus, "risk")
|
||||
|
||||
async def test_live_coach_short_circuits_strong_crisis_variants(self) -> None:
|
||||
client_replies = (
|
||||
"저는 지금 자살하려고 해요.",
|
||||
"지금 당장 죽으려고 해요.",
|
||||
"제 목숨을 끊으려고 해요.",
|
||||
"자살 계획이 있어요.",
|
||||
"자해 시도를 하려고 해요.",
|
||||
"자살 충동이 있어요.",
|
||||
"자살할지도 모르겠어요.",
|
||||
"스스로를 해칠 계획이 있어요.",
|
||||
"제 안전을 유지할 수 없어요.",
|
||||
)
|
||||
|
||||
for index, client_reply in enumerate(client_replies, start=1):
|
||||
with self.subTest(client_reply=client_reply):
|
||||
item = live_coach.LiveCoachInput(
|
||||
session_id=f"strong-crisis-variant-{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=AssertionError("strong crisis signal must bypass engine")
|
||||
)
|
||||
)
|
||||
|
||||
suggestion = await live_coach.generate_live_coaching(item, engine=engine)
|
||||
|
||||
engine.generate.assert_not_called()
|
||||
self.assertEqual(suggestion.focus, "risk")
|
||||
self.assertEqual(suggestion.status, "ready")
|
||||
|
||||
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