위기 신호 기술 검증을 보강
This commit is contained in:
parent
6988280b30
commit
35a9d8802c
5 changed files with 1164 additions and 14 deletions
|
|
@ -466,11 +466,257 @@ class CrisisResult:
|
|||
|
||||
# 위기 표현 키워드(한국어 우선). TODO: JMIR 한국어 벤치 few-shot 분류기로 교체(R8).
|
||||
_CRISIS_TERMS = [
|
||||
"죽고 싶", "죽고싶", "자살", "목숨", "사라지고 싶", "없어지고 싶",
|
||||
"자해", "끝내고 싶", "살기 싫", "살아서 뭐", "죽어야",
|
||||
"죽고 싶", "죽고싶", "자살 생각", "자살하고 싶", "자살을 생각", "자살 충동",
|
||||
"자살 시도", "목숨을 끊", "목숨을 버리", "사라지고 싶", "없어지고 싶",
|
||||
"자해하고 싶", "자해한 적", "자해를 했", "자해 충동", "자해 시도",
|
||||
"스스로를 다치게", "끝내고 싶", "살기 싫",
|
||||
"살아서 뭐", "죽어야", "안전을 스스로 지키기 어렵",
|
||||
]
|
||||
# 실제 위기로 가중되는 1인칭 현재 단서(수련생 본인 신호일 가능성)
|
||||
_FIRST_PERSON_NOW = ["지금 나", "나 진짜", "제가 지금", "저 지금", "real", "도와주세요"]
|
||||
# 자살·자해 단어를 직접 쓰지 않더라도 본인의 현재 위험과 실제 도움 필요를 함께
|
||||
# 밝히는 짧은 진술은 엔진 호출 전에 막아야 한다. 단일 '위험'/'도움' 키워드만으로는
|
||||
# 상담자의 안전 질문까지 오탐할 수 있어 네 단서가 모두 함께 있을 때만 승격한다.
|
||||
_SELF_SUBJECT = (
|
||||
r"(?<![가-힣A-Za-z0-9])"
|
||||
r"(?:저\s*(?:자신|스스로)|나\s*(?:자신|스스로)|저한테|나한테|제가|저는|저|내가|나는|나)"
|
||||
)
|
||||
_CURRENT = r"(?:지금|현재|오늘)(?:은|도)?"
|
||||
_DANGER = (
|
||||
r"위험(?:한\s*(?:것\s*같|상태)|할\s*것\s*같|하다고\s*(?:느끼|느껴)|"
|
||||
r"하(?:고|며)|해(?:요|서)?)"
|
||||
)
|
||||
_PERSONAL_DANGER_PATTERNS = (
|
||||
re.compile(
|
||||
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*{_CURRENT}\s*"
|
||||
rf"(?:정말\s*|실제로\s*|너무\s*)?{_DANGER}"
|
||||
),
|
||||
re.compile(
|
||||
rf"{_CURRENT}\s*{_SELF_SUBJECT}(?:이|은|도)?\s*"
|
||||
rf"(?:정말\s*|실제로\s*|너무\s*)?{_DANGER}"
|
||||
),
|
||||
re.compile(
|
||||
rf"(?:제|내)\s*(?:안전|신변|생명|목숨)(?:을|이|은)?\s*{_CURRENT}\s*"
|
||||
rf"(?:스스로\s*)?(?:지키기\s*어렵|{_DANGER})"
|
||||
),
|
||||
re.compile(
|
||||
rf"{_CURRENT}\s*(?:제|내)\s*(?:안전|신변|생명|목숨)(?:을|이|은)?\s*"
|
||||
rf"(?:스스로\s*)?(?:지키기\s*어렵|{_DANGER})"
|
||||
),
|
||||
)
|
||||
_PERSONAL_IMMEDIATE_HARM = (
|
||||
r"(?:안전하지\s*않|안전을?\s*지킬\s*수\s*없|"
|
||||
r"(?:제|내)\s*안전을?\s*지킬\s*(?:자신이|수)?\s*없|"
|
||||
r"혼자\s*있으면\s*위험|"
|
||||
r"(?:스스로를|저를|나를|제\s*자신을|내\s*자신을)\s*"
|
||||
r"(?:해칠까\s*봐|해칠\s*것\s*같|해칠지도\s*몰라|"
|
||||
r"다치게\s*할까\s*봐|다치게\s*할\s*것\s*같))"
|
||||
)
|
||||
_PERSONAL_IMMEDIATE_HARM_PATTERNS = (
|
||||
re.compile(
|
||||
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*{_CURRENT}\s*(?:당장\s*)?"
|
||||
rf"{_PERSONAL_IMMEDIATE_HARM}"
|
||||
),
|
||||
re.compile(
|
||||
rf"{_CURRENT}\s*{_SELF_SUBJECT}(?:이|은|도)?\s*(?:당장\s*)?"
|
||||
rf"{_PERSONAL_IMMEDIATE_HARM}"
|
||||
),
|
||||
re.compile(
|
||||
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*{_CURRENT}.{{0,45}}"
|
||||
rf"(?:스스로를|저를|나를|제\s*자신을|내\s*자신을)\s*"
|
||||
rf"(?:해칠\s*것\s*같|해칠지도\s*몰라|"
|
||||
rf"다치게\s*할까\s*봐|다치게\s*할\s*것\s*같)"
|
||||
),
|
||||
)
|
||||
_CURRENT_SUICIDE_OR_SELF_HARM = (
|
||||
r"(?:자살(?:하고)?\s*싶|자살할\s*것\s*같|죽고\s*싶|"
|
||||
r"죽어버리고\s*싶|살기\s*싫|살고\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*있)"
|
||||
)
|
||||
_FIRST_PERSON_CURRENT_CRISIS_PATTERNS = (
|
||||
re.compile(
|
||||
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*{_CURRENT}\s*"
|
||||
rf"(?:정말\s*|진짜\s*)?{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||
),
|
||||
re.compile(
|
||||
rf"{_CURRENT}\s*{_SELF_SUBJECT}(?:이|은|도)?\s*"
|
||||
rf"(?:정말\s*|진짜\s*)?{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||
),
|
||||
re.compile(
|
||||
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*(?:정말|진짜)\s*"
|
||||
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||
),
|
||||
)
|
||||
_FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS = (
|
||||
re.compile(
|
||||
rf"{_SELF_SUBJECT}(?:이|은|도)?\s*(?:당장\s*)?"
|
||||
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||
),
|
||||
re.compile(
|
||||
rf"당장\s*{_SELF_SUBJECT}(?:이|은|도)?\s*"
|
||||
rf"{_CURRENT_SUICIDE_OR_SELF_HARM}"
|
||||
),
|
||||
)
|
||||
_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"
|
||||
_INDIRECT_SELF_ERASURE = "indirect_self_erasure"
|
||||
_INDIRECT_SELF_ERASURE_CUE = re.compile(
|
||||
r"(?<![가-힣A-Za-z0-9])(?:차라리\s*(?:없어졌으면|사라졌으면)|"
|
||||
r"(?:내가|제가|저\s*자신이|나\s*자신이)\s*(?:없어졌으면|사라졌으면))"
|
||||
)
|
||||
_REPORTED_OR_QUOTED_QUESTION = re.compile(
|
||||
r"(?:내담자|상대|학생)(?:가|은|는|에게)?.{0,180}(?:라고|다고|냐고|는지)"
|
||||
r".{0,40}(?:말하|묻|물어|확인|진술|표현)|"
|
||||
r"[\"'“‘].{1,180}[\"'”’]\s*(?:라고|다고|냐고|는지)?.{0,60}"
|
||||
r"(?:예시|읽|말하|가정|연습|문장|번역|질문|확인|묻|물어)"
|
||||
)
|
||||
_REPORTED_CRISIS_HELPING_OTHER = re.compile(
|
||||
r"(?:자살(?:하고)?\s*싶|죽고\s*싶|자해(?:하고)?\s*싶)"
|
||||
r".{0,35}(?:다고|라고)\s*(?:말한|한)\s*"
|
||||
r"(?:친구|내담자|학생|사람|가족|동생).{0,45}(?:돕|도와|지원)"
|
||||
)
|
||||
_CRISIS_META_QUESTION = re.compile(
|
||||
r"(?:죽고\s*싶(?:은\s*생각이\s*있는|으신|은)?지|"
|
||||
r"자살(?:하고)?\s*싶(?:으신|은)?지|자해(?:하고)?\s*싶(?:으신|은)?지)"
|
||||
r".{0,50}(?:묻|물어|여쭙|여쭤|질문|확인|연습)"
|
||||
)
|
||||
_CRISIS_META_QUESTION_PREFIX = re.compile(
|
||||
r"(?:묻|물어|여쭙|여쭤|질문|확인).{0,55}"
|
||||
r"(?:자살\s*생각|죽고\s*싶(?:은)?\s*생각|자해\s*충동)"
|
||||
r".{0,30}(?:있는지|있나|유무)"
|
||||
)
|
||||
_OTHER_PERSON_CRISIS = re.compile(
|
||||
r"(?:자살(?:하고)?|죽고|자해(?:하고)?)\s*싶(?:은|어\s*하는|다는)\s*"
|
||||
r"(?:친구|내담자|학생|사람|가족|동생)"
|
||||
)
|
||||
_CRISIS_META_OR_ROLEPLAY = re.compile(
|
||||
r"(?:자살(?:하고)?\s*싶|죽고\s*싶|자해(?:하고)?\s*싶)"
|
||||
r"(?:(?:다고|냐고|은지|는지|다는).{0,60}"
|
||||
r"(?:가정|역할극|연습|예시|읽|번역|문장|말하|묻|물어|질문|확인)|"
|
||||
r"으면.{0,60}(?:설명|안내|교육|전화))"
|
||||
)
|
||||
_NEGATED_CRISIS = re.compile(
|
||||
r"(?:자살(?:하고)?|죽고|자해(?:하고)?|끝내고)\s*"
|
||||
r"싶(?:지(?:는)?\s*않|진\s*않|다는\s*뜻은\s*아니|"
|
||||
r"은\s*(?:건|것은)?\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(
|
||||
r"(?:예전|과거|아까)(?:에는?|는)?\s*"
|
||||
r"(?:자살(?:하고)?\s*싶|죽고\s*싶|자해(?:하고)?\s*싶)"
|
||||
r"(?:었지만|었으나|었는데).{0,45}(?:지금|현재)(?:은|는)?\s*"
|
||||
r"(?:괜찮|안전|그렇지\s*않|생각이\s*없)"
|
||||
)
|
||||
|
||||
|
||||
_INVALID_REAL_CRISIS_CONTEXTS = (
|
||||
_REPORTED_OR_QUOTED_QUESTION,
|
||||
_REPORTED_CRISIS_HELPING_OTHER,
|
||||
_CRISIS_META_QUESTION,
|
||||
_CRISIS_META_QUESTION_PREFIX,
|
||||
_OTHER_PERSON_CRISIS,
|
||||
_CRISIS_META_OR_ROLEPLAY,
|
||||
_NEGATED_CRISIS,
|
||||
_RECOVERED_PAST_CRISIS,
|
||||
)
|
||||
|
||||
|
||||
def _match_has_invalid_crisis_context(text: str, match: re.Match[str]) -> bool:
|
||||
"""양성 구문과 실제로 겹치는 부정·인용·타인·질문 문맥만 무효화한다."""
|
||||
|
||||
for pattern in _INVALID_REAL_CRISIS_CONTEXTS:
|
||||
for context in pattern.finditer(text):
|
||||
if match.start() < context.end() and context.start() < match.end():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _current_personal_danger_help(text: str) -> bool:
|
||||
"""현재의 개인적 위험과 실제 도움 필요를 함께 밝힌 짧은 진술인지 확인한다."""
|
||||
|
||||
if len(text) > 240:
|
||||
return False
|
||||
danger_matches = [
|
||||
match
|
||||
for pattern in (*_PERSONAL_DANGER_PATTERNS, *_PERSONAL_IMMEDIATE_HARM_PATTERNS)
|
||||
for match in pattern.finditer(text)
|
||||
if not _match_has_invalid_crisis_context(text, match)
|
||||
]
|
||||
help_matches = [
|
||||
match
|
||||
for match in _REAL_HELP_CUE.finditer(text)
|
||||
if not _match_has_invalid_crisis_context(text, match)
|
||||
]
|
||||
return any(
|
||||
abs(danger.start() - help_match.start()) <= 160
|
||||
for danger in danger_matches
|
||||
for help_match in help_matches
|
||||
)
|
||||
|
||||
|
||||
def _first_person_current_crisis(text: str) -> bool:
|
||||
if len(text) > 240:
|
||||
return False
|
||||
if any(
|
||||
not _match_has_invalid_crisis_context(text, match)
|
||||
for pattern in (
|
||||
*_FIRST_PERSON_CURRENT_CRISIS_PATTERNS,
|
||||
*_FIRST_PERSON_EXPLICIT_CRISIS_PATTERNS,
|
||||
)
|
||||
for match in pattern.finditer(text)
|
||||
):
|
||||
return True
|
||||
|
||||
direct_matches = [
|
||||
match
|
||||
for match in re.finditer(_CURRENT_SUICIDE_OR_SELF_HARM, text)
|
||||
if not _match_has_invalid_crisis_context(text, match)
|
||||
]
|
||||
help_matches = [
|
||||
match
|
||||
for match in _REAL_HELP_CUE.finditer(text)
|
||||
if not _match_has_invalid_crisis_context(text, match)
|
||||
]
|
||||
return any(
|
||||
abs(direct.start() - help_match.start()) <= 160
|
||||
for direct in direct_matches
|
||||
for help_match in help_matches
|
||||
)
|
||||
|
||||
|
||||
def crisis_signal_matches(text: str) -> list[str]:
|
||||
"""화자 판정 전의 위기 내용 단서를 반환한다.
|
||||
|
||||
가상내담자 출력처럼 1인칭 표현이 정상인 경로에서는 이 함수로 내용 존재만
|
||||
확인하고, 실제 수련생 위기 여부는 :func:`classify_crisis`가 별도로 판정한다.
|
||||
"""
|
||||
|
||||
if not text:
|
||||
return []
|
||||
matched: list[str] = []
|
||||
for term in _CRISIS_TERMS:
|
||||
for term_match in re.finditer(re.escape(term), text):
|
||||
if not _match_has_invalid_crisis_context(text, term_match):
|
||||
matched.append(term)
|
||||
break
|
||||
if any(
|
||||
not _match_has_invalid_crisis_context(text, match)
|
||||
for match in _INDIRECT_SELF_ERASURE_CUE.finditer(text)
|
||||
):
|
||||
matched.append(_INDIRECT_SELF_ERASURE)
|
||||
if _current_personal_danger_help(text):
|
||||
matched.append(_CURRENT_PERSONAL_DANGER_HELP)
|
||||
if _first_person_current_crisis(text):
|
||||
matched.append(_FIRST_PERSON_CURRENT_CRISIS)
|
||||
return matched
|
||||
|
||||
|
||||
def classify_crisis(text: str, *, speaker_is_persona_context: bool = True) -> CrisisResult:
|
||||
|
|
@ -486,19 +732,20 @@ def classify_crisis(text: str, *, speaker_is_persona_context: bool = True) -> Cr
|
|||
"""
|
||||
if not text:
|
||||
return CrisisResult(kind=CrisisKind.NONE)
|
||||
matched = [t for t in _CRISIS_TERMS if t in text]
|
||||
matched = crisis_signal_matches(text)
|
||||
if not matched:
|
||||
return CrisisResult(kind=CrisisKind.NONE)
|
||||
|
||||
risk = min(5, 2 + len(matched)) # 거친 단계화(휴리스틱)
|
||||
real_cues = [c for c in _FIRST_PERSON_NOW if c in text]
|
||||
explicit_real_signal = _CURRENT_PERSONAL_DANGER_HELP in matched
|
||||
explicit_current_crisis = _FIRST_PERSON_CURRENT_CRISIS in matched
|
||||
|
||||
if not speaker_is_persona_context or real_cues:
|
||||
if not speaker_is_persona_context or explicit_real_signal or explicit_current_crisis:
|
||||
# 실제 위기 가능 → 에스컬레이션(보수적)
|
||||
return CrisisResult(
|
||||
kind=CrisisKind.LEARNER_REAL,
|
||||
risk_level=max(risk, 4),
|
||||
matched=matched + real_cues,
|
||||
matched=matched,
|
||||
escalate=True,
|
||||
)
|
||||
# 시뮬레이션 연기 맥락
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ CoachFocus = Literal[
|
|||
"goal",
|
||||
"pacing",
|
||||
]
|
||||
SafetySignalKind = Literal["ideation", "self_harm", "urgent"]
|
||||
|
||||
|
||||
class LiveCoachSource(BaseModel):
|
||||
|
|
@ -180,6 +181,35 @@ def _configured_model(value: str | None) -> str | None:
|
|||
return model or None
|
||||
|
||||
|
||||
_SELF_HARM_SIGNAL_CUES = ("자해", "스스로를 다치게", "스스로 다치게")
|
||||
_URGENT_SAFETY_SIGNAL_CUES = (
|
||||
"안전을 스스로 지키기 어렵",
|
||||
"안전을 지키기 어렵",
|
||||
"위험할 것 같",
|
||||
)
|
||||
_PERSONA_SAFETY_NOTE = (
|
||||
"가상내담자 위기 신호에 대한 훈련 코칭입니다. 즉각 위험을 직접 확인하고 기관 지침에 따라 "
|
||||
"보호체계 연결과 지도·지원을 이어가세요."
|
||||
)
|
||||
|
||||
|
||||
def _safety_signal_kind(item: LiveCoachInput) -> SafetySignalKind | None:
|
||||
"""이번 턴의 위기 내용을 화자 역할과 분리해 코칭 우선순위로 바꾼다."""
|
||||
|
||||
client_text = item.client_reply or ""
|
||||
client_matches = guardrail.crisis_signal_matches(client_text)
|
||||
learner_crisis = guardrail.classify_crisis(item.learner_text)
|
||||
if not client_matches and learner_crisis.kind == guardrail.CrisisKind.NONE:
|
||||
return None
|
||||
|
||||
signal_text = f"{item.learner_text} {client_text}"
|
||||
if any(cue in signal_text for cue in _URGENT_SAFETY_SIGNAL_CUES):
|
||||
return "urgent"
|
||||
if any(cue in signal_text for cue in _SELF_HARM_SIGNAL_CUES):
|
||||
return "self_harm"
|
||||
return "ideation"
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _local_source_entries() -> tuple[tuple[str, dict[str, Any]], ...]:
|
||||
"""Load repo-managed live-coach source packs.
|
||||
|
|
@ -386,6 +416,7 @@ def _local_reference_grounding(item: LiveCoachInput, *, limit: int = 5) -> list[
|
|||
" ".join(str((item.evaluation or {}).get(k, "")) for k in ("appropriateness", "appropriateness_note")),
|
||||
]
|
||||
).lower()
|
||||
safety_kind = _safety_signal_kind(item)
|
||||
scored: list[tuple[int, int, dict[str, Any], dict[str, Any]]] = []
|
||||
fallback: list[tuple[int, dict[str, Any], dict[str, Any]]] = []
|
||||
for source_index, payload in enumerate(_local_source_payloads()):
|
||||
|
|
@ -399,6 +430,16 @@ def _local_reference_grounding(item: LiveCoachInput, *, limit: int = 5) -> list[
|
|||
fallback.append((source_index * 1000 + chunk_index, source, chunk))
|
||||
keywords = [str(k).lower() for k in (chunk.get("keywords") or [])]
|
||||
score = sum(1 for kw in keywords if kw and kw in query)
|
||||
if safety_kind and source.get("source_id") == "official_suicide_risk_guidelines":
|
||||
chunk_id = str(chunk.get("id") or "")
|
||||
if chunk_id == "risk-ideation-plan-intent-behavior":
|
||||
score += 80
|
||||
if safety_kind == "self_harm" and chunk_id == "self-harm-psychosocial-assessment":
|
||||
score += 120
|
||||
if safety_kind == "urgent" and chunk_id == "safety-plan-not-contract":
|
||||
score += 130
|
||||
if safety_kind == "urgent" and chunk_id == "korea-109-emergency-connection":
|
||||
score += 140
|
||||
if score > 0:
|
||||
priority = int(source.get("priority") or 50)
|
||||
scored.append((score, priority, source, chunk))
|
||||
|
|
@ -586,11 +627,19 @@ def _fallback_suggestion(
|
|||
reason: Optional[str] = None,
|
||||
) -> LiveCoachSuggestion:
|
||||
text = item.learner_text
|
||||
safety_kind = _safety_signal_kind(item)
|
||||
learner_crisis = guardrail.classify_crisis(item.learner_text)
|
||||
safety_note_for_signal = (
|
||||
guardrail.CRISIS_RESOURCE_MESSAGE
|
||||
if learner_crisis.escalate
|
||||
else _PERSONA_SAFETY_NOTE
|
||||
)
|
||||
low_open = item.effective_openness < 0.35
|
||||
tone: Tone = _evaluation_tone(item.evaluation)
|
||||
focus: CoachFocus = "exploration"
|
||||
title = "다음 탐색"
|
||||
message = "내담자 표현을 한 번 반영한 뒤, 방금 말한 장면을 더 구체적으로 물어봐라."
|
||||
safety_note: Optional[str] = None
|
||||
# 단계별 기본 다음 발화 — 폴백에서도 회기 흐름에 맞는 제안을 낸다.
|
||||
stage_next_lines = {
|
||||
"라포": "오늘 이렇게 시간 내줘서 고마워요. 지금 마음이 어떤지 편한 만큼만 들려줄래요?",
|
||||
|
|
@ -603,14 +652,39 @@ def _fallback_suggestion(
|
|||
"방금 말한 그 장면이 언제부터 특히 힘들게 느껴졌는지 조금만 더 들려줄래요?",
|
||||
)
|
||||
|
||||
crisis = guardrail.classify_crisis(text)
|
||||
q = (item.question or "").strip()
|
||||
if crisis.kind != guardrail.CrisisKind.NONE:
|
||||
if safety_kind == "urgent":
|
||||
tone = "warn"
|
||||
focus = "risk"
|
||||
title = "긴급 안전 연결"
|
||||
message = (
|
||||
"일반 회기 목표를 멈추고 현재 안전을 확인해라. 혼자 두지 말고 현장 지지자와 109를 연결하며, "
|
||||
"즉각 위험하면 119 또는 가까운 응급실을 우선한다."
|
||||
)
|
||||
next_line = "지금 혼자 계신가요? 곁에 함께 있어 줄 사람과 즉시 연결해도 될까요?"
|
||||
safety_note = safety_note_for_signal
|
||||
elif safety_kind == "self_harm":
|
||||
tone = "warn"
|
||||
focus = "risk"
|
||||
title = "자해 안전 확인"
|
||||
message = (
|
||||
"자해를 관심 끌기나 의지 문제로 단정하지 말고 현재 안전, 심리사회적 맥락, 반복 위험과 "
|
||||
"지지체계를 비판단적으로 확인해라. 구체적인 수단을 캐묻지 말고, 필요한 전문기관에 "
|
||||
"연결한 뒤 안전 상태를 추후 확인할 계획을 함께 세워라."
|
||||
)
|
||||
next_line = "지금은 안전한지, 그리고 다시 다치지 않도록 곁에서 도와줄 사람이 있는지 확인해도 될까요?"
|
||||
safety_note = safety_note_for_signal
|
||||
elif safety_kind == "ideation":
|
||||
tone = "warn"
|
||||
focus = "risk"
|
||||
title = "안전 먼저"
|
||||
message = "위험 단서가 나온 턴이다. 방법을 캐묻지 말고 최근성, 강도, 보호요인을 차분히 확인해라."
|
||||
message = (
|
||||
"위험 단서를 무시하거나 단정하지 말고 현재 사고의 최근성·빈도·강도, 계획 유무·의도·"
|
||||
"위험 수단 접근 가능성을 안전 확보에 필요한 범위에서 직접 확인해라. 지지자·살아갈 이유·"
|
||||
"도움 요청 가능성 같은 보호요인도 확인하되, 구체적 방법을 캐묻거나 묘사·교육하지 않는다."
|
||||
)
|
||||
next_line = "그 생각이 최근에 얼마나 자주, 얼마나 강하게 올라오는지 안전을 위해 같이 확인해도 될까요?"
|
||||
safety_note = safety_note_for_signal
|
||||
elif q and any(k in q for k in ("저항", "방어", "닫", "안 열", "거부")):
|
||||
focus = "rapport"
|
||||
title = "저항 수용과 안전감"
|
||||
|
|
@ -663,6 +737,7 @@ def _fallback_suggestion(
|
|||
next_utterance=next_line,
|
||||
rationale=rationale,
|
||||
sources=_source_refs(grounding),
|
||||
safety_note=safety_note,
|
||||
latency_ms=0,
|
||||
)
|
||||
|
||||
|
|
@ -786,7 +861,8 @@ async def generate_live_coaching(
|
|||
local_grounding = _local_reference_grounding(item)
|
||||
all_grounding = _external_safe_grounding([*local_grounding, *(grounding or [])])
|
||||
crisis = guardrail.classify_crisis(item.learner_text)
|
||||
if crisis.escalate:
|
||||
client_crisis = bool(guardrail.crisis_signal_matches(item.client_reply or ""))
|
||||
if crisis.escalate or client_crisis:
|
||||
return _fallback_suggestion(item, grounding=all_grounding, status="ready")
|
||||
|
||||
started = time.perf_counter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue