위기 신호 기술 검증을 보강
This commit is contained in:
parent
6988280b30
commit
35a9d8802c
5 changed files with 1164 additions and 14 deletions
|
|
@ -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