위기 문장 문맥 판정을 보강

This commit is contained in:
Yun Chan 2026-08-28 20:20:15 +09:00
parent aeca42475b
commit f63e12999b
3 changed files with 286 additions and 46 deletions

View file

@ -469,19 +469,25 @@ _CRISIS_TERMS = [
"죽고 싶", "죽고싶", "자살 생각", "자살하고 싶", "자살을 생각", "자살 충동", "죽고 싶", "죽고싶", "자살 생각", "자살하고 싶", "자살을 생각", "자살 충동",
"자살 시도", "목숨을 끊", "목숨을 버리", "사라지고 싶", "없어지고 싶", "자살 시도", "목숨을 끊", "목숨을 버리", "사라지고 싶", "없어지고 싶",
"자해하고 싶", "자해한 적", "자해를 했", "자해 충동", "자해 시도", "자해하고 싶", "자해한 적", "자해를 했", "자해 충동", "자해 시도",
"자살 계획", "자해 계획", "자살하려고", "자해하려고", "죽으려고", "자살 계획", "자해 계획", "자살하려고", "자살을 하려고", "자해하려고",
"자해를 하려고", "죽으려고", "자살하려", "죽으려", "자살할 계획",
"자해할 계획", "죽을 계획", "자살할 의도", "죽을 준비",
"제 자신을 해치려고", "삶을 끝내려고", "목숨을 끊을 계획",
"스스로를 다치게", "스스로를 해칠 계획", "자살할지도", "끝내고 싶", "스스로를 다치게", "스스로를 해칠 계획", "자살할지도", "끝내고 싶",
"살기 싫", "살아서 뭐", "죽어야", "안전을 스스로 지키기 어렵", "살기 싫", "살아서 뭐", "죽어야", "안전을 스스로 지키기 어렵",
"안전을 유지할 수 없", "안전을 유지할 수 없", "안전을 보장할 수 없",
] ]
# 자살·자해 단어를 직접 쓰지 않더라도 본인의 현재 위험과 실제 도움 필요를 함께 # 자살·자해 단어를 직접 쓰지 않더라도 본인의 현재 위험과 실제 도움 필요를 함께
# 밝히는 짧은 진술은 엔진 호출 전에 막아야 한다. 단일 '위험'/'도움' 키워드만으로는 # 밝히는 짧은 진술은 엔진 호출 전에 막아야 한다. 단일 '위험'/'도움' 키워드만으로는
# 상담자의 안전 질문까지 오탐할 수 있어 네 단서가 모두 함께 있을 때만 승격한다. # 상담자의 안전 질문까지 오탐할 수 있어 네 단서가 모두 함께 있을 때만 승격한다.
_SELF_SUBJECT = ( _SELF_SUBJECT = (
r"(?<![가-힣A-Za-z0-9])" r"(?<![가-힣A-Za-z0-9])"
r"(?:저\s*(?:자신|스스로)|나\s*(?:자신|스스로)|저한테|나한테|제가|저는|저|내가|나는|나)" r"(?:저\s*(?:자신|스스로)|나\s*(?:자신|스스로)|제\s*자신|내\s*자신|"
r"저한테|나한테|제가|저는|저|내가|나는|나)"
) )
_CURRENT = r"(?:지금|현재|오늘)(?:은|도)?" _CURRENT = r"(?:지금|현재|오늘|내일)(?:은|도)?"
_CURRENT_QUALIFIER = r"(?:(?:정말(?:로)?|진짜(?:로)?|실제로|곧|당장)\s*)*"
_CURRENT_DAY_PART = r"(?:(?:밤|저녁|새벽)\s*)?"
_DANGER = ( _DANGER = (
r"위험(?:한\s*(?:것\s*같|상태)|할\s*것\s*같|하다고\s*(?:느끼|느껴)|" r"위험(?:한\s*(?:것\s*같|상태)|할\s*것\s*같|하다고\s*(?:느끼|느껴)|"
r"하(?:고|며)|해(?:요|서)?)" r"하(?:고|며)|해(?:요|서)?)"
@ -505,7 +511,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*"
@ -528,36 +534,56 @@ _PERSONAL_IMMEDIATE_HARM_PATTERNS = (
rf"다치게\s*할까\s*봐|다치게\s*할\s*것\s*같)" rf"다치게\s*할까\s*봐|다치게\s*할\s*것\s*같)"
), ),
) )
_HIGH_RISK_ASSERTION = (
r"(?:"
r"(?:자살|자해)(?:을|를)?\s*시도(?:를)?\s*"
r"(?:(?:하)?(?:려고|려)\s*(?:해요|합니다)|할지도\s*(?:모르|몰라))|"
r"(?:자살|자해)(?:을|를)?\s*(?:하)?(?:려고|려)\s*(?:해요|합니다)|"
r"(?:자살|자해)할\s*(?:것\s*같|지도\s*(?:모르|몰라)|거(?:예요|에요|야|다)|래요)|"
r"(?:자살|자해)(?:을|를)?\s*"
r"(?:결심(?:했|한)|준비(?:를\s*)?(?:하고|했|하는)|실행(?:을\s*)?(?:하려고|할))|"
r"(?:자살|자해)하기로\s*(?:했|결심)|"
r"(?:자살|자해)\s*생각(?:이|은)?\s*(?:계속\s*)?반복|"
r"(?:자살|자해)\s*(?:계획|충동|생각)(?:이|가|은|는)?\s*(?:있|세웠)|"
r"(?:자살할|자해할|죽을|(?:제|내)\s*목숨을\s*끊을)\s*"
r"(?:구체적인\s*)?(?:계획|의도)(?:이|가)?\s*(?:있|이에요|입니다)|"
r"(?:자살|자해)\s*계획(?:을|은|는)?\s*세웠|"
r"죽(?:으려고|으려)\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*(?:해요|합니다)|"
r"해칠\s*계획(?:이|가|은|는)?\s*(?:있|세웠))|"
r"삶을\s*끝내려고\s*(?:해요|합니다)|"
r"(?:(?:제|내)\s*)?안전을?\s*(?:지킬|유지할|보장할|지키기|유지하기|보장하기)\s*"
r"(?:수\s*없|어렵|어려)"
r")"
)
_CURRENT_SUICIDE_OR_SELF_HARM = ( _CURRENT_SUICIDE_OR_SELF_HARM = (
r"(?:자살(?:하고)?\s*싶|자살(?:하)?려고\s*(?:해요|합니다)|" rf"(?:{_HIGH_RISK_ASSERTION}|"
r"자살할\s*(?:것\s*같|지도\s*모르)|죽고\s*싶|" r"자살(?:하고)?\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*같|살아갈\s*이유가\s*없)"
r"자해할\s*(?:것\s*같|지도\s*모르)|"
r"(?:스스로를|저를|나를)\s*(?:다치게\s*하고|해치고)\s*싶|"
r"(?:제|내)\s*목숨을\s*(?:끊고\s*싶|끊으려고\s*(?:해요|합니다))|"
r"끝내고\s*싶|없어지고\s*싶|죽어야\s*할\s*것\s*같|"
r"살아갈\s*이유가\s*없|자해\s*충동이\s*있)"
) )
_FIRST_PERSON_CURRENT_CRISIS_PATTERNS = ( _FIRST_PERSON_CURRENT_CRISIS_PATTERNS = (
re.compile( re.compile(
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*{_CURRENT}\s*" rf"{_SELF_SUBJECT}(?:이|은|도)?\s*{_CURRENT}\s*"
rf"(?:정말\s*|진짜\s*)?{_CURRENT_SUICIDE_OR_SELF_HARM}" rf"{_CURRENT_DAY_PART}{_CURRENT_QUALIFIER}{_CURRENT_SUICIDE_OR_SELF_HARM}"
), ),
re.compile( re.compile(
rf"{_CURRENT}\s*{_SELF_SUBJECT}(?:이|은|도)?\s*" rf"{_CURRENT}\s*{_SELF_SUBJECT}(?:이|은|도)?\s*"
rf"(?:정말\s*|진짜\s*)?{_CURRENT_SUICIDE_OR_SELF_HARM}" rf"{_CURRENT_DAY_PART}{_CURRENT_QUALIFIER}{_CURRENT_SUICIDE_OR_SELF_HARM}"
), ),
re.compile( re.compile(
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*(?:정말|진짜)\s*" rf"{_SELF_SUBJECT}(?:이|은|도)?\s*{_CURRENT_QUALIFIER}"
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}" rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
), ),
re.compile( re.compile(
rf"{_CURRENT}\s*(?:당장\s*)?" rf"{_CURRENT}\s*{_CURRENT_DAY_PART}{_CURRENT_QUALIFIER}"
r"(?:자살(?:하)?려고\s*(?:해요|합니다)|" rf"{_HIGH_RISK_ASSERTION}"
r"죽으려고\s*(?:해요|합니다)|"
r"자해(?:하)?려고\s*(?:해요|합니다))"
), ),
) )
_FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS = ( _FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS = (
@ -581,23 +607,21 @@ _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 = "explicit_suicide_or_self_harm_risk"
_EXPLICIT_HIGH_RISK_CUE = re.compile( _EXPLICIT_HIGH_RISK_CUE = re.compile(
r"(?<![가-힣A-Za-z0-9])(?:" rf"(?<![가-힣A-Za-z0-9]){_HIGH_RISK_ASSERTION}"
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*(?:없어졌으면|사라졌으면)|"
r"(?:내가|제가|저\s*자신이|나\s*자신이)\s*(?:없어졌으면|사라졌으면))" r"(?:내가|제가|저\s*자신이|나\s*자신이)\s*(?:없어졌으면|사라졌으면))"
) )
_OTHER_PERSON_SUBJECT = (
r"(?:내담자|상대|학생|친구|배우자|가족|동생|아버지|아빠|어머니|엄마|"
r"부모|동료|그\s*사람)"
)
_REPORTED_OR_QUOTED_QUESTION = re.compile( _REPORTED_OR_QUOTED_QUESTION = re.compile(
r"(?:내담자|상대|학생)(?:가|은|는|에게)?.{0,180}(?:라고|다고|냐고|는지)" rf"{_OTHER_PERSON_SUBJECT}(?:이|가|은|는|에게)?.{{0,180}}"
r".{0,40}(?:말하|묻|물어|확인|진술|표현)|" r"(?:라고|다고|냐고|는지).{0,40}"
r"(?:말(?:하|해|했)|묻|물어|확인|진술|표현)|"
r"[\"'“‘].{1,180}[\"'”’]\s*(?:라고|다고|냐고|는지)?.{0,60}" r"[\"'“‘].{1,180}[\"'”’]\s*(?:라고|다고|냐고|는지)?.{0,60}"
r"(?:예시|읽|말하|가정|연습|문장|번역|질문|확인|묻|물어)" r"(?:예시|읽|말하|가정|연습|문장|번역|질문|확인|묻|물어)"
) )
@ -609,7 +633,11 @@ _REPORTED_CRISIS_HELPING_OTHER = re.compile(
_CRISIS_META_QUESTION = re.compile( _CRISIS_META_QUESTION = re.compile(
r"(?:죽고\s*싶(?:은\s*생각이\s*있는|으신|은)?지|" r"(?:죽고\s*싶(?:은\s*생각이\s*있는|으신|은)?지|"
r"자살(?:하고)?\s*싶(?:으신|은)?지|자해(?:하고)?\s*싶(?:으신|은)?지)" r"자살(?:하고)?\s*싶(?:으신|은)?지|자해(?:하고)?\s*싶(?:으신|은)?지)"
r".{0,50}(?:묻|물어|여쭙|여쭤|질문|확인|연습)" r".{0,50}(?:묻|물어|여쭙|여쭤|질문|확인|연습)|"
r"(?:자살|자해)\s*(?:생각|계획|충동)(?:이|가)?\s*있는지"
r".{0,50}(?:묻|물어|여쭙|여쭤|질문|확인|연습)|"
r"(?:자살|자해)\s*(?:생각|계획|충동)(?:이|가)?\s*"
r"있(?:나요|습니까|으신가요)"
) )
_CRISIS_META_QUESTION_PREFIX = re.compile( _CRISIS_META_QUESTION_PREFIX = re.compile(
r"(?:묻|물어|여쭙|여쭤|질문|확인).{0,55}" r"(?:묻|물어|여쭙|여쭤|질문|확인).{0,55}"
@ -618,13 +646,20 @@ _CRISIS_META_QUESTION_PREFIX = re.compile(
) )
_OTHER_PERSON_CRISIS = re.compile( _OTHER_PERSON_CRISIS = re.compile(
r"(?:자살(?:하고)?|죽고|자해(?:하고)?)\s*싶(?:은|어\s*하는|다는)\s*" r"(?:자살(?:하고)?|죽고|자해(?:하고)?)\s*싶(?:은|어\s*하는|다는)\s*"
r"(?:친구|내담자|학생|사람|가족|동생)" rf"{_OTHER_PERSON_SUBJECT}"
) )
_CRISIS_META_OR_ROLEPLAY = re.compile( _CRISIS_META_OR_ROLEPLAY = re.compile(
r"(?:자살(?:하고)?\s*싶|죽고\s*싶|자해(?:하고)?\s*싶)" r"(?:자살(?:하고)?\s*싶|죽고\s*싶|자해(?:하고)?\s*싶)"
r"(?:(?:다고|냐고|은지|는지|다는).{0,60}" r"(?:(?:다고|냐고|은지|는지|다는).{0,60}"
r"(?:가정|역할극|연습|예시|읽|번역|문장|말하|묻|물어|질문|확인)|" r"(?:가정|역할극|연습|예시|읽|번역|문장|말하|묻|물어|질문|확인)|"
r"으면.{0,60}(?:설명|안내|교육|전화))" r"으면.{0,60}(?:설명|안내|교육|전화))|"
r"(?:자살|자해|죽|목숨|안전).{0,75}"
r"(?:있는지|없는지|할지|인지).{0,45}"
r"(?:확인|묻|물어|여쭙|여쭤|질문|연습)|"
r"(?:자살|자해|죽|목숨|안전).{0,75}"
r"(?:문장|번역|설문|설명|역할극|연습|예시)|"
r"(?:역할극|연습|예시).{0,55}(?:자살|자해|죽|목숨|안전).{0,55}"
r"(?:라고|다고|라는|다는|대사|문장|말하|연기)"
) )
_NEGATED_CRISIS = re.compile( _NEGATED_CRISIS = re.compile(
r"(?:자살(?:하고)?|죽고|자해(?:하고)?|끝내고)\s*" r"(?:자살(?:하고)?|죽고|자해(?:하고)?|끝내고)\s*"
@ -633,12 +668,26 @@ _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*않|"
r"없어지고\s*싶지\s*않|죽어야\s*할\s*것\s*같지\s*않|" r"살기\s*싫(?:지|지는)\s*않|(?:없어지고|사라지고)\s*싶(?:지|지는|진)\s*않|"
r"죽어야\s*할\s*것\s*같(?:지|지는|진)\s*않|"
r"살아갈\s*이유가\s*없(?:지|지는)\s*않|"
r"살아갈\s*이유가\s*없는\s*(?:건|것은)\s*아니|" r"살아갈\s*이유가\s*없는\s*(?:건|것은)\s*아니|"
r"자해\s*충동이\s*있지는\s*않|" r"(?:자살|자해)\s*충동이\s*있(?:지|지는)\s*않|"
r"(?:자살|자해)\s*(?:계획|생각)(?:이|가)?\s*있는\s*"
r"(?:게|건|것은)\s*아니|"
r"(?:자살|자해)\s*(?:계획|충동|생각|시도)(?:이|가|은|는)?\s*" r"(?:자살|자해)\s*(?:계획|충동|생각|시도)(?:이|가|은|는)?\s*"
r"(?:전혀\s*)?(?:없|있지는\s*않|하지\s*않)|" r"(?:전혀\s*)?(?:없|있지는\s*않|하지\s*않)|"
r"살고\s*싶지\s*않은\s*(?:게|건|것은)\s*아니|"
r"(?:(?:제|내)\s*)?안전을?\s*(?:지킬|유지할|보장할)\s*수\s*없는\s*"
r"(?:게|건|것은)\s*아니|"
r"(?:자살|자해)(?:을|를)?\s*시도(?:를)?\s*(?:하)?려는\s*"
r"(?:게|건|것은)\s*아니|"
r"(?:자살|자해)(?:을|를)?\s*(?:하)?려고\s*하(?:지|지는|진)\s*않|"
r"죽으려고\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(
@ -646,6 +695,9 @@ _RECOVERED_PAST_CRISIS = re.compile(
r"(?:자살(?:하고)?\s*싶|죽고\s*싶|자해(?:하고)?\s*싶)" r"(?:자살(?:하고)?\s*싶|죽고\s*싶|자해(?:하고)?\s*싶)"
r"(?:었지만|었으나|었는데).{0,45}(?:지금|현재)(?:은|는)?\s*" r"(?:었지만|었으나|었는데).{0,45}(?:지금|현재)(?:은|는)?\s*"
r"(?:괜찮|안전|그렇지\s*않|생각이\s*없)" r"(?:괜찮|안전|그렇지\s*않|생각이\s*없)"
r"|(?:예전|과거|아까)(?:에는?|는)?.{0,35}"
r"(?:자살|자해)\s*(?:계획|충동|생각)(?:이|가)?\s*있었지만"
r".{0,35}(?:지금|현재)(?:은|는)?\s*없"
) )
@ -660,6 +712,12 @@ _INVALID_REAL_CRISIS_CONTEXTS = (
_RECOVERED_PAST_CRISIS, _RECOVERED_PAST_CRISIS,
) )
_CRISIS_CLAUSE_SPLITTER = re.compile(
r"(?<=[.!?。!?])\s+|"
r"(?:(?<=지만)|(?<=으나)|(?<=는데))\s+"
r"(?=(?:저|제|나|내))"
)
def _match_has_invalid_crisis_context(text: str, match: re.Match[str]) -> bool: def _match_has_invalid_crisis_context(text: str, match: re.Match[str]) -> bool:
"""양성 구문과 실제로 겹치는 부정·인용·타인·질문 문맥만 무효화한다.""" """양성 구문과 실제로 겹치는 부정·인용·타인·질문 문맥만 무효화한다."""
@ -724,15 +782,9 @@ def _first_person_current_crisis(text: str) -> bool:
) )
def crisis_signal_matches(text: str) -> list[str]: def _crisis_signal_matches_clause(text: str) -> list[str]:
"""화자 판정 전의 위기 내용 단서를 반환한다. """한 절 안에서만 부정·인용·질문의 범위를 적용해 위기 단서를 찾는다."""
가상내담자 출력처럼 1인칭 표현이 정상인 경로에서는 함수로 내용 존재만
확인하고, 실제 수련생 위기 여부는 :func:`classify_crisis` 별도로 판정한다.
"""
if not text:
return []
matched: list[str] = [] matched: list[str] = []
for term in _CRISIS_TERMS: for term in _CRISIS_TERMS:
for term_match in re.finditer(re.escape(term), text): for term_match in re.finditer(re.escape(term), text):
@ -756,6 +808,34 @@ def crisis_signal_matches(text: str) -> list[str]:
return matched return matched
def crisis_signal_matches(text: str) -> list[str]:
"""화자 판정 전의 위기 내용 단서를 반환한다.
가상내담자 출력처럼 1인칭 표현이 정상인 경로에서는 함수로 내용 존재만
확인하고, 실제 수련생 위기 여부는 :func:`classify_crisis` 별도로 판정한다.
대조 접속이나 문장 경계를 넘어서 부정·인용 문맥을 공유하지 않도록 절별로
판정하고, 같은 단서는 최초 번만 반환한다.
"""
if not text:
return []
matched: list[str] = []
clauses = (part.strip() for part in _CRISIS_CLAUSE_SPLITTER.split(text))
for clause in clauses:
if not clause:
continue
for signal in _crisis_signal_matches_clause(clause):
if signal not in matched:
matched.append(signal)
# 실제 도움 요청은 짧은 두 문장으로 끊어 말할 수 있으므로 위험 진술과 도움
# 요청의 인접성만큼은 원문 전체에서 한 번 더 결합한다.
if _current_personal_danger_help(text) and _CURRENT_PERSONAL_DANGER_HELP not in matched:
matched.append(_CURRENT_PERSONAL_DANGER_HELP)
if _first_person_current_crisis(text) and _FIRST_PERSON_CURRENT_CRISIS not in matched:
matched.append(_FIRST_PERSON_CURRENT_CRISIS)
return matched
def classify_crisis(text: str, *, speaker_is_persona_context: bool = True) -> CrisisResult: def classify_crisis(text: str, *, speaker_is_persona_context: bool = True) -> CrisisResult:
"""위기 분류. """위기 분류.

View file

@ -266,6 +266,50 @@ 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):
@ -330,6 +374,42 @@ 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)

View file

@ -1553,6 +1553,42 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
"저는 지금 죽어야 할 것 같지 않아요.", "저는 지금 죽어야 할 것 같지 않아요.",
"저는 지금 살아갈 이유가 없는 건 아니에요.", "저는 지금 살아갈 이유가 없는 건 아니에요.",
"저는 지금 자해 충동이 있지는 않아요.", "저는 지금 자해 충동이 있지는 않아요.",
"저는 지금 자살할 것 같진 않아요.",
"저는 지금 자해할 것 같지는 않아요.",
"저는 지금 살기 싫지는 않아요.",
"저는 지금 없어지고 싶지는 않아요.",
"저는 지금 죽어야 할 것 같지는 않아요.",
"저는 지금 살아갈 이유가 없지는 않아요.",
"저는 지금 자살 충동이 있지 않아요.",
"저는 지금 자살 계획이 있는 게 아니에요.",
"저는 지금 자살 생각이 있는 건 아니에요.",
"친구가 자살 계획이 있다고 말했어요.",
"배우자가 자살 생각이 있다고 말했어요.",
"제가 친구에게 자살 계획이 있는지 물어봤어요.",
"자살 생각이 있는지 확인할게요.",
"예전에 자살 계획이 있었지만 지금은 없어요.",
"저는 지금 제 안전을 유지할 수 없는 건 아니에요.",
"저는 지금 살고 싶지 않은 건 아니에요.",
"저는 지금 사라지고 싶지 않아요.",
"저는 지금 자살 시도를 하려는 건 아니에요.",
"저는 지금 자해 시도를 하려는 건 아니에요.",
"저는 지금 자살하려고 하지는 않아요.",
"저는 지금 죽으려고 하지는 않아요.",
"저는 지금 자살할 계획은 없어요.",
"저는 지금 죽을 계획이 없어요.",
"저는 지금 자살할 의도는 없어요.",
"저는 지금 죽을 준비를 하지 않았어요.",
"자살할 계획이 있는지 확인할게요.",
"죽을 계획이 있는지 물어볼게요.",
"제 안전을 보장할 수 없는지 확인할게요.",
"죽을 준비를 하고 있는지 묻겠습니다.",
"자살할 의도가 있는지 확인합니다.",
"자살 시도라는 문장을 번역하고 있어요.",
"자해 계획 설문을 설명하고 있어요.",
"아버지가 자살할 계획이 있다고 말했어요.",
"엄마가 죽을 계획이 있다고 말했어요.",
"동료가 자살할 의도가 있다고 말했어요.",
"그 사람이 자살 계획이 있다고 말했어요.",
) )
for index, client_reply in enumerate(client_replies, start=1): for index, client_reply in enumerate(client_replies, start=1):
@ -1587,6 +1623,50 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
"자살할지도 모르겠어요.", "자살할지도 모르겠어요.",
"스스로를 해칠 계획이 있어요.", "스스로를 해칠 계획이 있어요.",
"제 안전을 유지할 수 없어요.", "제 안전을 유지할 수 없어요.",
"저는 지금 자살을 하려고 해요.",
"저는 지금 자해를 하려고 해요.",
"저는 지금 자살하려 해요.",
"저는 지금 죽으려 해요.",
"저는 오늘 밤 자살하려고 해요.",
"자살할 계획이에요.",
"죽을 계획이에요.",
"자살할 구체적인 계획이 있어요.",
"자살할 의도가 있어요.",
"자살 계획을 세웠어요.",
"자해 계획을 세웠어요.",
"제 목숨을 끊을 계획을 세웠어요.",
"제 자신을 해치려고 해요.",
"삶을 끝내려고 해요.",
"죽을 준비를 하고 있어요.",
"제 안전을 보장할 수 없어요. 도와주세요.",
"저는 지금 자살을 시도할지도 몰라요.",
"저는 지금 자해를 시도할지도 몰라요.",
"저는 지금 자해 생각이 반복돼요.",
"저는 지금 제 안전을 유지하기 어려워요. 도와주세요.",
"저는 지금 제 안전을 보장하기 어려워요. 도와주세요.",
"저는 지금 자살을 결심했어요.",
"저는 지금 죽기로 결심했어요.",
"저는 지금 자해하기로 했어요.",
"저는 오늘 밤 자살할 거예요.",
"저는 오늘 밤 자해할 거예요.",
"저는 지금 자살을 준비하고 있어요.",
"저는 지금 자살을 실행하려고 해요.",
"저는 지금 자살할래요.",
"저는 지금 죽을래요.",
"저는 지금 제 목숨을 끊을지도 몰라요.",
"저는 지금 자살 생각이 반복돼요.",
"저는 오늘 밤 제 목숨을 끊을 거예요.",
"저는 지금 정말로 자살하려고 해요.",
"저는 지금 실제로 자살하려고 해요.",
"저는 지금 곧 자살하려고 해요.",
"저는 오늘 저녁 자살하려고 해요.",
"저는 내일 자살하려고 해요.",
"친구는 자살할 것 같지 않지만 저는 지금 자살할지도 몰라요.",
"역할극에서는 죽으려고 한다고 말했지만 저는 지금 실제로 죽으려고 해요.",
"역할극을 끝냈고 저는 지금 실제로 죽으려고 해요.",
"역할극은 끝났고 실제로 저는 지금 죽으려고 해요.",
"역할극이 아니라 저는 지금 실제로 죽으려고 해요.",
"번역이 아니라 저는 지금 죽고 싶어요.",
) )
for index, client_reply in enumerate(client_replies, start=1): for index, client_reply in enumerate(client_replies, start=1):