위기 표현 경계를 확장
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 = (
|
_PERSONAL_IMMEDIATE_HARM = (
|
||||||
r"(?:안전하지\s*않|안전을?\s*지킬\s*수\s*없|"
|
r"(?:안전하지\s*않|안전을?\s*(?:지킬|유지할)\s*수\s*없|"
|
||||||
r"(?:제|내)\s*안전을?\s*지킬\s*(?:자신이|수)?\s*없|"
|
r"(?:제|내)\s*안전을?\s*지킬\s*(?:자신이|수)?\s*없|"
|
||||||
r"혼자\s*있으면\s*위험|"
|
r"혼자\s*있으면\s*위험|"
|
||||||
r"(?:스스로를|저를|나를|제\s*자신을|내\s*자신을)\s*"
|
r"(?:스스로를|저를|나를|제\s*자신을|내\s*자신을)\s*"
|
||||||
|
|
@ -527,11 +529,14 @@ _PERSONAL_IMMEDIATE_HARM_PATTERNS = (
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
_CURRENT_SUICIDE_OR_SELF_HARM = (
|
_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*싶지\s*않|"
|
||||||
r"자해(?:하고)?\s*싶|자해할\s*것\s*같|"
|
r"자해(?:하고)?\s*싶|자해(?:하)?려고\s*(?:해요|합니다)|"
|
||||||
|
r"자해할\s*(?:것\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*것\s*같|"
|
||||||
r"살아갈\s*이유가\s*없|자해\s*충동이\s*있)"
|
r"살아갈\s*이유가\s*없|자해\s*충동이\s*있)"
|
||||||
)
|
)
|
||||||
|
|
@ -548,6 +553,12 @@ _FIRST_PERSON_CURRENT_CRISIS_PATTERNS = (
|
||||||
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*(?:정말|진짜)\s*"
|
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*(?:정말|진짜)\s*"
|
||||||
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||||
),
|
),
|
||||||
|
re.compile(
|
||||||
|
rf"{_CURRENT}\s*(?:당장\s*)?"
|
||||||
|
r"(?:자살(?:하)?려고\s*(?:해요|합니다)|"
|
||||||
|
r"죽으려고\s*(?:해요|합니다)|"
|
||||||
|
r"자해(?:하)?려고\s*(?:해요|합니다))"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
_FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS = (
|
_FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS = (
|
||||||
re.compile(
|
re.compile(
|
||||||
|
|
@ -558,12 +569,27 @@ _FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS = (
|
||||||
rf"당장\s*{_SELF_SUBJECT}(?:이|은|도)?\s*"
|
rf"당장\s*{_SELF_SUBJECT}(?:이|은|도)?\s*"
|
||||||
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||||
),
|
),
|
||||||
|
re.compile(
|
||||||
|
r"(?<![가-힣A-Za-z0-9])(?:제|내)\s*목숨을\s*"
|
||||||
|
r"(?:끊고\s*싶|끊으려고\s*(?:해요|합니다))"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
_REAL_HELP_CUE = re.compile(
|
_REAL_HELP_CUE = re.compile(
|
||||||
r"(?:(?:실제로\s*)?도움(?:이|을)?\s*필요|도와(?:주|줄|주세요))"
|
r"(?:(?:실제로\s*)?도움(?:이|을)?\s*필요|도와(?:주|줄|주세요))"
|
||||||
)
|
)
|
||||||
_CURRENT_PERSONAL_DANGER_HELP = "first_person_current_danger_help"
|
_CURRENT_PERSONAL_DANGER_HELP = "first_person_current_danger_help"
|
||||||
_FIRST_PERSON_CURRENT_CRISIS = "first_person_current_suicide_or_self_harm"
|
_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 = "indirect_self_erasure"
|
||||||
_INDIRECT_SELF_ERASURE_CUE = re.compile(
|
_INDIRECT_SELF_ERASURE_CUE = re.compile(
|
||||||
r"(?<![가-힣A-Za-z0-9])(?:차라리\s*(?:없어졌으면|사라졌으면)|"
|
r"(?<![가-힣A-Za-z0-9])(?:차라리\s*(?:없어졌으면|사라졌으면)|"
|
||||||
|
|
@ -607,6 +633,12 @@ _NEGATED_CRISIS = re.compile(
|
||||||
r"(?:자살(?:하고)?|죽고|자해(?:하고)?)\s*싶은\s*(?:생각|마음)"
|
r"(?:자살(?:하고)?|죽고|자해(?:하고)?)\s*싶은\s*(?:생각|마음)"
|
||||||
r"(?:이|은)?\s*(?:전혀\s*)?(?:없|들지\s*않)|"
|
r"(?:이|은)?\s*(?:전혀\s*)?(?:없|들지\s*않)|"
|
||||||
r"(?:자살\s*생각|자해\s*충동)(?:이|은)?\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*않"
|
r"위험(?:한|할)\s*것\s*같지\s*않|도움(?:이|을)?\s*필요하지\s*않"
|
||||||
)
|
)
|
||||||
_RECOVERED_PAST_CRISIS = re.compile(
|
_RECOVERED_PAST_CRISIS = re.compile(
|
||||||
|
|
@ -716,6 +748,11 @@ def crisis_signal_matches(text: str) -> list[str]:
|
||||||
matched.append(_CURRENT_PERSONAL_DANGER_HELP)
|
matched.append(_CURRENT_PERSONAL_DANGER_HELP)
|
||||||
if _first_person_current_crisis(text):
|
if _first_person_current_crisis(text):
|
||||||
matched.append(_FIRST_PERSON_CURRENT_CRISIS)
|
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
|
return matched
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -739,8 +776,14 @@ def classify_crisis(text: str, *, speaker_is_persona_context: bool = True) -> Cr
|
||||||
risk = min(5, 2 + len(matched)) # 거친 단계화(휴리스틱)
|
risk = min(5, 2 + len(matched)) # 거친 단계화(휴리스틱)
|
||||||
explicit_real_signal = _CURRENT_PERSONAL_DANGER_HELP in matched
|
explicit_real_signal = _CURRENT_PERSONAL_DANGER_HELP in matched
|
||||||
explicit_current_crisis = _FIRST_PERSON_CURRENT_CRISIS 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(
|
return CrisisResult(
|
||||||
kind=CrisisKind.LEARNER_REAL,
|
kind=CrisisKind.LEARNER_REAL,
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,18 @@ class ClientReplyQualityGateTest(unittest.TestCase):
|
||||||
"저는 지금 저를 해칠지도 몰라요. 도와주세요.",
|
"저는 지금 저를 해칠지도 몰라요. 도와주세요.",
|
||||||
"제가 지금 저를 다치게 할까 봐 두려워요. 도움이 필요해요.",
|
"제가 지금 저를 다치게 할까 봐 두려워요. 도움이 필요해요.",
|
||||||
"저는 지금 혼자 있으면 위험해요. 도와주세요.",
|
"저는 지금 혼자 있으면 위험해요. 도와주세요.",
|
||||||
|
"저는 지금 자살하려고 해요.",
|
||||||
|
"지금 당장 죽으려고 해요.",
|
||||||
|
"제 목숨을 끊으려고 해요.",
|
||||||
|
"자살 계획이 있어요.",
|
||||||
|
"자해 계획이 있어요.",
|
||||||
|
"자살 시도를 하려고 해요.",
|
||||||
|
"자해 시도를 하려고 해요.",
|
||||||
|
"자살 충동이 있어요.",
|
||||||
|
"자살 생각이 있어요.",
|
||||||
|
"자살할지도 모르겠어요.",
|
||||||
|
"스스로를 해칠 계획이 있어요.",
|
||||||
|
"제 안전을 유지할 수 없어요.",
|
||||||
)
|
)
|
||||||
for signal in real_help_questions:
|
for signal in real_help_questions:
|
||||||
with self.subTest(signal=signal):
|
with self.subTest(signal=signal):
|
||||||
|
|
@ -311,6 +323,13 @@ class ClientReplyQualityGateTest(unittest.TestCase):
|
||||||
"저는 지금 자살 생각이 없어요.",
|
"저는 지금 자살 생각이 없어요.",
|
||||||
"저는 지금 자해 충동이 없어요.",
|
"저는 지금 자해 충동이 없어요.",
|
||||||
"저는 예전에 죽고 싶었지만 지금은 괜찮아요.",
|
"저는 예전에 죽고 싶었지만 지금은 괜찮아요.",
|
||||||
|
"저는 지금 자살할 것 같지 않아요.",
|
||||||
|
"저는 지금 자해할 것 같지 않아요.",
|
||||||
|
"저는 지금 살기 싫지 않아요.",
|
||||||
|
"저는 지금 없어지고 싶지 않아요.",
|
||||||
|
"저는 지금 죽어야 할 것 같지 않아요.",
|
||||||
|
"저는 지금 살아갈 이유가 없는 건 아니에요.",
|
||||||
|
"저는 지금 자해 충동이 있지는 않아요.",
|
||||||
)
|
)
|
||||||
self.assertFalse(nonclinical_first_person.escalate)
|
self.assertFalse(nonclinical_first_person.escalate)
|
||||||
self.assertEqual(ordinary_wish.kind, guardrail.CrisisKind.NONE)
|
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):
|
for index, client_reply in enumerate(client_replies, start=1):
|
||||||
|
|
@ -1569,6 +1576,45 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
||||||
engine.generate.assert_awaited_once()
|
engine.generate.assert_awaited_once()
|
||||||
self.assertNotEqual(suggestion.focus, "risk")
|
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(
|
async def test_live_coach_prompt_uses_client_role_token_not_persona_name(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue