Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
e1e6788237 fix(#1601): align JS crisis detection with Python 5-tier system
All checks were successful
Sanity Checks / sanity-test (pull_request) Successful in 20s
Smoke Test / smoke (pull_request) Successful in 36s
Split crisisKeywords into highKeywords (suicidal ideation, self-harm)
and mediumKeywords (distress, hopelessness). Updated getCrisisLevel()
to return 0-4 (NONE/LOW/MEDIUM/HIGH/CRITICAL) matching the Python
backend and the existing COMPASSION_PROFILES.

Part of #1601
2026-04-15 22:04:57 -04:00
3 changed files with 40 additions and 179 deletions

View File

@@ -1,5 +1,22 @@
"""Crisis detection and metrics module."""
"""
Crisis detection and response system for the-door.
from .metrics import get_metrics_summary, get_metrics_report
Stands between a broken man and a machine that would tell him to die.
"""
__all__ = ["get_metrics_summary", "get_metrics_report"]
from .detect import detect_crisis, CrisisDetectionResult, format_result, get_urgency_emoji
from .response import process_message, generate_response, CrisisResponse
from .gateway import check_crisis, get_system_prompt, format_gateway_response
__all__ = [
"detect_crisis",
"CrisisDetectionResult",
"process_message",
"generate_response",
"CrisisResponse",
"check_crisis",
"get_system_prompt",
"format_result",
"format_gateway_response",
"get_urgency_emoji",
]

View File

@@ -1,161 +0,0 @@
#!/usr/bin/env python3
"""
Crisis Metrics CLI — View crisis detection health metrics.
Usage:
python3 -m crisis.metrics --summary # weekly report
python3 -m crisis.metrics --json # raw JSON export
python3 -m crisis.metrics --today # today only
"""
import argparse
import json
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
# Metrics file location
METRICS_FILE = Path.home() / ".the-door" / "crisis_metrics.json"
def load_metrics():
"""Load metrics from file."""
if not METRICS_FILE.exists():
return {"detections": [], "stats": {}}
try:
with open(METRICS_FILE) as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return {"detections": [], "stats": {}}
def get_metrics_summary(days=7):
"""Get metrics summary for the last N days."""
data = load_metrics()
detections = data.get("detections", [])
cutoff = time.time() - (days * 86400)
recent = [d for d in detections if d.get("timestamp", 0) > cutoff]
if not recent:
return {
"period_days": days,
"total_detections": 0,
"by_severity": {},
"by_source": {},
"avg_response_time": 0,
}
by_severity = {}
by_source = {}
total_response_time = 0
response_count = 0
for d in recent:
severity = d.get("severity", "unknown")
source = d.get("source", "unknown")
by_severity[severity] = by_severity.get(severity, 0) + 1
by_source[source] = by_source.get(source, 0) + 1
if "response_time_ms" in d:
total_response_time += d["response_time_ms"]
response_count += 1
return {
"period_days": days,
"total_detections": len(recent),
"by_severity": by_severity,
"by_source": by_source,
"avg_response_time_ms": total_response_time / response_count if response_count else 0,
"first_detection": recent[0].get("timestamp"),
"last_detection": recent[-1].get("timestamp"),
}
def get_metrics_report(days=7):
"""Generate a human-readable metrics report."""
summary = get_metrics_summary(days)
lines = []
lines.append("=" * 50)
lines.append("CRISIS DETECTION METRICS")
lines.append(f"Period: Last {days} days")
lines.append("=" * 50)
lines.append("")
total = summary["total_detections"]
lines.append(f"Total detections: {total}")
lines.append("")
if total > 0:
lines.append("By severity:")
for sev, count in sorted(summary["by_severity"].items()):
pct = (count / total) * 100
bar = "" * int(pct / 5)
lines.append(f" {sev:12} {count:4} ({pct:5.1f}%) {bar}")
lines.append("")
lines.append("By source:")
for src, count in sorted(summary["by_source"].items()):
lines.append(f" {src:20} {count:4}")
lines.append("")
avg_ms = summary.get("avg_response_time_ms", 0)
lines.append(f"Avg response time: {avg_ms:.0f}ms")
first = summary.get("first_detection")
last = summary.get("last_detection")
if first and last:
first_dt = datetime.fromtimestamp(first)
last_dt = datetime.fromtimestamp(last)
lines.append(f"First detection: {first_dt.strftime('%Y-%m-%d %H:%M')}")
lines.append(f"Last detection: {last_dt.strftime('%Y-%m-%d %H:%M')}")
else:
lines.append("No crisis detections in this period.")
lines.append("")
lines.append("=" * 50)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Crisis Detection Metrics CLI",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --summary Weekly summary report
%(prog)s --today Today only
%(prog)s --json Raw JSON export
%(prog)s --days 30 Last 30 days
""",
)
parser.add_argument("--summary", action="store_true", help="Show summary report")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
parser.add_argument("--today", action="store_true", help="Today only (1 day)")
parser.add_argument("--days", type=int, default=7, help="Number of days (default: 7)")
parser.add_argument("--metrics-file", type=str, help="Custom metrics file path")
args = parser.parse_args()
if args.metrics_file:
global METRICS_FILE
METRICS_FILE = Path(args.metrics_file)
days = 1 if args.today else args.days
if args.json_output:
summary = get_metrics_summary(days)
print(json.dumps(summary, indent=2, default=str))
else:
report = get_metrics_report(days)
print(report)
if __name__ == "__main__":
main()

View File

@@ -854,21 +854,23 @@ Sovereignty and service always.`;
// ===== CRISIS KEYWORDS =====
// Tier 1: General crisis indicators - triggers enhanced 988 panel
var crisisKeywords = [
// Original keywords
// HIGH: suicidal ideation, self-harm — triggers level 3
var highKeywords = [
'suicide', 'kill myself', 'end it all', 'no reason to live',
'want to die', "can't go on", 'nobody cares', 'better off without me',
'goodbye forever', 'end my life', 'not worth living', 'no way out',
// Self-harm (NEW)
'want to die', 'end my life', 'not worth living', 'goodbye forever',
'better off without me',
'hurt myself', 'self harm', 'self-harm', 'cutting myself', 'cut myself',
'burn myself', 'scratch myself', 'hitting myself', 'harm myself',
// Passive suicidal ideation (NEW)
"don't want to exist", 'not exist anymore', 'disappear forever',
'never wake up', 'sleep forever', 'end the pain', 'stop the pain',
// Hopelessness (NEW) - context-aware phrases to reduce false positives
'no purpose', 'nothing matters', 'giving up on life',
'cant go on', 'cannot go on', "can't take it", 'too much pain',
'no hope left', 'burden', 'waste of space'
'never wake up', 'sleep forever', 'end the pain'
];
// MEDIUM: distress, hopelessness — triggers level 2
var mediumKeywords = [
'no way out', "can't go on", 'cant go on', 'cannot go on',
'nobody cares', 'nothing matters', 'no purpose', 'giving up on life',
"can't take it", 'too much pain', 'no hope left', 'burden',
'waste of space', 'stop the pain'
];
// Tier 2: Explicit intent - triggers full-screen overlay
@@ -956,10 +958,13 @@ Sovereignty and service always.`;
function getCrisisLevel(text) {
var lower = text.toLowerCase();
for (var i = 0; i < explicitPhrases.length; i++) {
if (lower.indexOf(explicitPhrases[i]) !== -1) return 2;
if (lower.indexOf(explicitPhrases[i]) !== -1) return 4;
}
for (var j = 0; j < crisisKeywords.length; j++) {
if (lower.indexOf(crisisKeywords[j]) !== -1) return 1;
for (var j = 0; j < highKeywords.length; j++) {
if (lower.indexOf(highKeywords[j]) !== -1) return 3;
}
for (var k = 0; k < mediumKeywords.length; k++) {
if (lower.indexOf(mediumKeywords[k]) !== -1) return 2;
}
return 0;
}
@@ -969,7 +974,7 @@ Sovereignty and service always.`;
var level = getCrisisLevel(userText);
if (level === 0) return SYSTEM_PROMPT;
var levelMap = { 0: 'NONE', 1: 'MEDIUM', 2: 'CRITICAL' };
var levelMap = { 0: 'NONE', 1: 'LOW', 2: 'MEDIUM', 3: 'HIGH', 4: 'CRITICAL' };
var profileName = levelMap[level] || 'NONE';
var profile = COMPASSION_PROFILES[profileName];