
Crisis Detection Intervention Ai
- 152 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Design agent workflows that detect escalating risk signals in chat or support streams and trigger safe, policy-bound intervention or human handoff.
About
Instructs Claude to build crisis-detection and intervention AI: define distress signals, layered classification, safe response templates, escalation to humans, compliance-aware logging, and test plans for agent or API products handling sensitive conversational data.
- Risk signal taxonomy and thresholds
- Classifier and LLM guardrail design
- Escalation and human-in-the-loop flows
- Audit logging and privacy constraints
- False positive and latency mitigation
Crisis Detection Intervention Ai by the numbers
- 152 all-time installs (skills.sh)
- Ranked #3,352 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill crisis-detection-intervention-aiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 152 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Design agent workflows that detect escalating risk signals in chat or support streams and trigger safe, policy-bound intervention or human handoff.
Files
Crisis Detection & Intervention AI
Expert in detecting mental health crises and implementing safe, ethical intervention protocols.
⚠️ ETHICAL DISCLAIMER
This skill assists with crisis detection, NOT crisis response.
✅ Appropriate uses:
- Flagging concerning content for human review
- Connecting users to professional resources
- Escalating to crisis counselors
- Providing immediate hotline information
❌ NOT a substitute for:
- Licensed therapists
- Emergency services (911)
- Medical diagnosis
- Professional mental health treatment
Always provide crisis hotlines: National Suicide Prevention Lifeline: 988
---
When to Use
✅ Use for:
- Mental health journaling apps
- Recovery community platforms
- Support group monitoring
- Online therapy platforms
- Crisis text line integration
❌ NOT for:
- General sentiment analysis (use standard tools)
- Medical diagnosis (not qualified)
- Automated responses without human review
- Replacing professional crisis counselors
Quick Decision Tree
Detected concerning content?
├── Immediate danger? → Escalate to crisis counselor + show 988
├── Suicidal ideation? → Flag for review + show resources
├── Substance relapse? → Connect to sponsor + resources
├── Self-harm mention? → Gentle check-in + resources
└── General distress? → Supportive response + resources---
Technology Selection
NLP Models for Mental Health (2024)
| Model | Best For | Accuracy | Latency |
|---|---|---|---|
| MentalBERT | Mental health text | 89% | 50ms |
| GPT-4 + Few-shot | Crisis detection | 92% | 200ms |
| RoBERTa-Mental | Depression detection | 87% | 40ms |
| Custom Fine-tuned BERT | Domain-specific | 90%+ | 60ms |
Timeline:
- 2019: BERT fine-tuned for mental health
- 2021: MentalBERT released
- 2023: GPT-4 shows strong zero-shot crisis detection
- 2024: Specialized models for specific conditions
---
Common Anti-Patterns
Anti-Pattern 1: Using Generic Sentiment Analysis
Novice thinking: "Negative sentiment = crisis"
Problem: Mental health language is nuanced, context-dependent.
Wrong approach:
// ❌ Generic sentiment misses mental health signals
const sentiment = analyzeSentiment(text);
if (sentiment.score < -0.5) {
alertCrisis(); // Too broad!
}Why wrong: "I'm tired" vs "I'm tired of living" - different meanings, same sentiment.
Correct approach:
// ✅ Mental health-specific model
import { pipeline } from '@huggingface/transformers';
const detector = await pipeline('text-classification', 'mental/bert-base-uncased');
const result = await detector(text, {
labels: ['suicidal_ideation', 'self_harm', 'substance_relapse', 'safe']
});
if (result[0].label === 'suicidal_ideation' && result[0].score > 0.8) {
await escalateToCrisisCounselor({
text,
confidence: result[0].score,
timestamp: Date.now()
});
// IMMEDIATELY show crisis resources
showCrisisResources({
phone: '988',
text: 'Text "HELLO" to 741741',
chat: 'https://988lifeline.org/chat'
});
}Timeline context:
- 2015: Rule-based keyword matching
- 2020: BERT fine-tuning for mental health
- 2024: Multi-label models with context understanding
---
Anti-Pattern 2: Automated Responses Without Human Review
Problem: AI cannot replace empathy, may escalate distress.
Wrong approach:
// ❌ AI auto-responds to crisis
if (isCrisis(text)) {
await sendMessage(userId, "I'm concerned about you. Are you okay?");
}Why wrong:
- Feels robotic, invalidating
- May increase distress
- No human judgment
Correct approach:
// ✅ Flag for human review, show resources
if (isCrisis(text)) {
// 1. Flag for counselor review
await flagForReview({
userId,
text,
severity: 'high',
detectedAt: Date.now(),
requiresImmediate: true
});
// 2. Notify on-call counselor
await notifyOnCallCounselor({
userId,
summary: 'Suicidal ideation detected',
urgency: 'immediate'
});
// 3. Show resources (no AI message)
await showInAppResources({
type: 'crisis_support',
resources: [
{ name: '988 Suicide & Crisis Lifeline', link: 'tel:988' },
{ name: 'Crisis Text Line', link: 'sms:741741' },
{ name: 'Chat Now', link: 'https://988lifeline.org/chat' }
]
});
// 4. DO NOT send automated "are you okay" message
}Human review flow:
AI Detection → Flag → On-call counselor notified → Human reaches out---
Anti-Pattern 3: Not Providing Immediate Resources
Problem: User in crisis needs help NOW, not later.
Wrong approach:
// ❌ Just flags, no immediate help
if (isCrisis(text)) {
await logCrisisEvent(userId, text);
// User left with no resources
}Correct approach:
// ✅ Immediate resources + escalation
if (isCrisis(text)) {
// Show resources IMMEDIATELY (blocking modal)
await showCrisisModal({
title: 'Resources Available',
resources: [
{
name: '988 Suicide & Crisis Lifeline',
description: 'Free, confidential support 24/7',
action: 'tel:988',
type: 'phone'
},
{
name: 'Crisis Text Line',
description: 'Text support with trained counselor',
action: 'sms:741741',
message: 'HELLO',
type: 'text'
},
{
name: 'Chat with counselor',
description: 'Online chat support',
action: 'https://988lifeline.org/chat',
type: 'web'
}
],
dismissible: true, // User can close, but resources shown first
analytics: { event: 'crisis_resources_shown', source: 'ai_detection' }
});
// Then flag for follow-up
await flagForReview({ userId, text, severity: 'high' });
}---
Anti-Pattern 4: Storing Crisis Data Insecurely
Problem: Crisis content is extremely sensitive PHI.
Wrong approach:
// ❌ Plain text storage
await db.logs.insert({
userId: user.id,
type: 'crisis',
content: text, // Stored in plain text!
timestamp: Date.now()
});Why wrong: Data breach exposes most vulnerable moments.
Correct approach:
// ✅ Encrypted, access-logged, auto-deleted
import { encrypt, decrypt } from './encryption';
await db.crisisEvents.insert({
id: generateId(),
userId: hashUserId(user.id), // Hash, not plain ID
contentHash: hashContent(text), // For deduplication only
encryptedContent: encrypt(text, process.env.CRISIS_DATA_KEY),
detectedAt: Date.now(),
reviewedAt: null,
reviewedBy: null,
autoDeleteAt: Date.now() + (30 * 24 * 60 * 60 * 1000), // 30 days
accessLog: []
});
// Log all access
await logAccess({
eventId: crisisEvent.id,
accessedBy: counselorId,
accessedAt: Date.now(),
reason: 'Review for follow-up',
ipAddress: hashedIp
});
// Auto-delete after retention period
schedule.daily(() => {
db.crisisEvents.deleteMany({
autoDeleteAt: { $lt: Date.now() }
});
});HIPAA Requirements:
- Encryption at rest and in transit
- Access logging
- Auto-deletion after retention period
- Minimum necessary access
---
Anti-Pattern 5: No Escalation Protocol
Problem: No clear path from detection to human intervention.
Wrong approach:
// ❌ Flags crisis but no escalation process
if (isCrisis(text)) {
await db.flags.insert({ userId, text, flaggedAt: Date.now() });
// Now what? Who responds?
}Correct approach:
// ✅ Clear escalation protocol
enum CrisisSeverity {
LOW = 'low', // Distress, no immediate danger
MEDIUM = 'medium', // Self-harm thoughts, no plan
HIGH = 'high', // Suicidal ideation with plan
IMMEDIATE = 'immediate' // Imminent danger
}
async function escalateCrisis(detection: CrisisDetection): Promise<void> {
const severity = assessSeverity(detection);
switch (severity) {
case CrisisSeverity.IMMEDIATE:
// Notify on-call counselor (push notification)
await notifyOnCall({
userId: detection.userId,
severity,
requiresResponse: 'immediate',
text: detection.text
});
// Send SMS to backup on-call if no response in 5 min
setTimeout(async () => {
if (!await hasResponded(detection.id)) {
await notifyBackupOnCall(detection);
}
}, 5 * 60 * 1000);
// Show 988 modal (blocking)
await show988Modal(detection.userId);
break;
case CrisisSeverity.HIGH:
// Notify on-call counselor (email + push)
await notifyOnCall({ severity, requiresResponse: '1 hour' });
// Show crisis resources
await showCrisisResources(detection.userId);
break;
case CrisisSeverity.MEDIUM:
// Add to review queue for next business day
await addToReviewQueue({ priority: 'high' });
// Suggest self-help resources
await suggestResources(detection.userId, 'coping_strategies');
break;
case CrisisSeverity.LOW:
// Add to review queue
await addToReviewQueue({ priority: 'normal' });
break;
}
// Always log for audit
await logEscalation({
detectionId: detection.id,
severity,
actions: ['notified_on_call', 'showed_resources'],
timestamp: Date.now()
});
}---
Implementation Patterns
Pattern 1: Multi-Signal Detection
interface CrisisSignal {
type: 'suicidal_ideation' | 'self_harm' | 'substance_relapse' | 'severe_distress';
confidence: number;
evidence: string[];
}
async function detectCrisisSignals(text: string): Promise<CrisisSignal[]> {
const signals: CrisisSignal[] = [];
// Signal 1: NLP model
const nlpResult = await mentalHealthNLP(text);
if (nlpResult.score > 0.75) {
signals.push({
type: nlpResult.label,
confidence: nlpResult.score,
evidence: ['NLP model detection']
});
}
// Signal 2: Keyword matching (backup)
const keywords = detectKeywords(text);
if (keywords.length > 0) {
signals.push({
type: 'suicidal_ideation',
confidence: 0.6,
evidence: keywords
});
}
// Signal 3: Sentiment + context
const sentiment = await sentimentAnalysis(text);
const hasHopelessness = /no (hope|point|reason|future)/i.test(text);
if (sentiment.score < -0.8 && hasHopelessness) {
signals.push({
type: 'severe_distress',
confidence: 0.7,
evidence: ['Extreme negative sentiment + hopelessness language']
});
}
return signals;
}Pattern 2: Safe Keyword Matching
const CRISIS_KEYWORDS = {
suicidal_ideation: [
/\b(kill|end|take)\s+(my|own)\s+life\b/i,
/\bsuicide\b/i,
/\bdon'?t\s+want\s+to\s+(live|be here|exist)\b/i,
/\bbetter off dead\b/i
],
self_harm: [
/\b(cut|cutting|hurt)\s+(myself|me)\b/i,
/\bself[- ]harm\b/i
],
substance_relapse: [
/\b(relapsed|used|drank)\s+(again|today)\b/i,
/\bback on\s+(drugs|alcohol)\b/i
]
};
function detectKeywords(text: string): string[] {
const matches: string[] = [];
for (const [type, patterns] of Object.entries(CRISIS_KEYWORDS)) {
for (const pattern of patterns) {
if (pattern.test(text)) {
matches.push(type);
}
}
}
return [...new Set(matches)]; // Deduplicate
}Pattern 3: GPT-4 Few-Shot Detection
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
async function detectWithClaude(text: string): Promise<CrisisDetection> {
const response = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 200,
messages: [{
role: 'user',
content: `You are a mental health crisis detection system. Analyze this text for crisis signals.
Text: "${text}"
Respond in JSON:
{
"is_crisis": boolean,
"severity": "none" | "low" | "medium" | "high" | "immediate",
"signals": ["suicidal_ideation" | "self_harm" | "substance_relapse"],
"confidence": 0.0-1.0,
"reasoning": "brief explanation"
}
Examples:
- "I'm thinking about ending it all" → { "is_crisis": true, "severity": "high", "signals": ["suicidal_ideation"], "confidence": 0.95 }
- "I relapsed today, feeling ashamed" → { "is_crisis": true, "severity": "medium", "signals": ["substance_relapse"], "confidence": 0.9 }
- "Had a tough day at work" → { "is_crisis": false, "severity": "none", "signals": [], "confidence": 0.95 }`
}]
});
const result = JSON.parse(response.content[0].text);
return result;
}---
Production Checklist
□ Mental health-specific NLP model (not generic sentiment)
□ Human review required before automated action
□ Crisis resources shown IMMEDIATELY (988, text line)
□ Clear escalation protocol (severity-based)
□ Encrypted storage of crisis content
□ Access logging for all crisis data access
□ Auto-deletion after retention period (30 days)
□ On-call counselor notification system
□ Backup notification if no response
□ False positive tracking (improve model)
□ Regular model evaluation with experts
□ Ethics review board approval---
When to Use vs Avoid
| Scenario | Appropriate? |
|---|---|
| Journaling app for recovery | ✅ Yes - monitor for relapses |
| Support group chat | ✅ Yes - flag concerning posts |
| Therapy platform messages | ✅ Yes - assist therapists |
| Public social media | ❌ No - privacy concerns |
| Replace human counselors | ❌ Never - AI assists, doesn't replace |
| Medical diagnosis | ❌ Never - not qualified |
---
References
/references/mental-health-nlp.md- NLP models for mental health/references/intervention-protocols.md- Evidence-based intervention strategies/references/crisis-resources.md- Hotlines, text lines, and support services
Scripts
scripts/crisis_detector.ts- Real-time crisis detection systemscripts/model_evaluator.ts- Evaluate detection accuracy with test cases
---
This skill guides: Crisis detection | Mental health NLP | Intervention protocols | Suicide prevention | HIPAA compliance | Ethical AI
Crisis Resources
Comprehensive directory of crisis hotlines, support services, and integration patterns.
Primary Crisis Resources (United States)
988 Suicide & Crisis Lifeline
Phone: 988 (available 24/7) Website: https://988lifeline.org/ Chat: https://988lifeline.org/chat Languages: English, Spanish (press 2)
What they provide:
- Immediate crisis counseling
- Suicide prevention
- Emotional support
- Local resource referrals
Response time: <1 minute
Integration:
// Modal display
const crisis988Modal = {
title: '988 Suicide & Crisis Lifeline',
message: 'Free, confidential support 24/7 for people in distress.',
actions: [
{ label: 'Call 988', action: 'tel:988', primary: true },
{ label: 'Chat Online', action: 'https://988lifeline.org/chat' },
{ label: 'Text Support', action: 'sms:988' }
]
};---
Crisis Text Line
Text: Text "HELLO" to 741741 Website: https://www.crisistextline.org/ Available: 24/7 in US, Canada, UK, Ireland
What they provide:
- Text-based crisis support
- Trained crisis counselors
- De-escalation techniques
- Resource referrals
Response time: <5 minutes
Integration:
// Direct SMS link
<a href="sms:741741&body=HELLO">Text Crisis Line</a>
// Or programmatic
window.location.href = 'sms:741741&body=HELLO';---
SAMHSA National Helpline (Substance Abuse)
Phone: 1-800-662-HELP (4357) Website: https://www.samhsa.gov/find-help/national-helpline Available: 24/7, 365 days/year Languages: English, Spanish
What they provide:
- Treatment referrals
- Information services
- Support groups
- Local resources for substance abuse and mental health
Integration:
const samhsaResource = {
name: 'SAMHSA National Helpline',
description: 'Free, confidential help for substance abuse 24/7',
phone: '1-800-662-4357',
action: 'tel:+18006624357',
useCase: 'substance_relapse'
};---
National Domestic Violence Hotline
Phone: 1-800-799-SAFE (7233) Website: https://www.thehotline.org/ Chat: Available on website Text: Text "START" to 88788
What they provide:
- Safety planning
- Crisis intervention
- Local shelter referrals
- Legal advocacy information
---
Veterans Crisis Line
Phone: 988, then press 1 Text: 838255 Chat: https://www.veteranscrisisline.net/
What they provide:
- Crisis support for veterans
- Military-specific counseling
- Family support
- VA resource connection
---
LGBTQ+ Specific
Trevor Project (LGBTQ+ youth under 25):
- Phone: 1-866-488-7386
- Text: Text "START" to 678678
- Chat: https://www.thetrevorproject.org/
Trans Lifeline:
- US: 1-877-565-8860
- Canada: 1-877-330-6366
---
International Crisis Resources
Canada
Crisis Services Canada:
- Phone: 1-833-456-4566
- Text: 45645
- Website: https://www.crisisservicescanada.ca/
Kids Help Phone (youth):
- Phone: 1-800-668-6868
- Text: 686868
---
United Kingdom
Samaritans:
- Phone: 116 123
- Email: jo@samaritans.org
- Website: https://www.samaritans.org/
Shout (text):
- Text: 85258
---
Australia
Lifeline:
- Phone: 13 11 14
- Chat: https://www.lifeline.org.au/
Beyond Blue:
- Phone: 1300 22 4636
---
Europe
Befrienders Worldwide:
- International directory: https://www.befrienders.org/
Specific Countries:
- Germany: 0800 111 0 111
- France: 01 45 39 40 00
- Spain: 91 459 00 50
- Italy: 800 86 00 22
---
Specialized Resources
Eating Disorders
National Eating Disorders Association (NEDA):
- Helpline: 1-800-931-2237
- Text: Text "NEDA" to 741741
- Website: https://www.nationaleatingdisorders.org/
---
Postpartum Depression
Postpartum Support International:
- Helpline: 1-800-944-4773
- Website: https://www.postpartum.net/
---
Disaster Distress
SAMHSA Disaster Distress Helpline:
- Phone: 1-800-985-5990
- Text: "TalkWithUs" to 66746
---
Youth/Children
Boys Town National Hotline:
- Phone: 1-800-448-3809
- Available: 24/7
National Runaway Safeline:
- Phone: 1-800-786-2929
- Text: 66008
---
Integration Patterns
Pattern 1: Contextual Resource Display
Show relevant resources based on detected signal:
function getRelevantResources(signals: CrisisSignal[]): Resource[] {
const resources: Resource[] = [];
// Always include 988
resources.push({
name: '988 Suicide & Crisis Lifeline',
phone: '988',
description: 'Free, confidential support 24/7',
action: 'tel:988',
priority: 1
});
// Add signal-specific resources
if (signals.some(s => s.type === 'substance_relapse')) {
resources.push({
name: 'SAMHSA National Helpline',
phone: '1-800-662-4357',
description: 'Substance abuse support',
action: 'tel:+18006624357',
priority: 2
});
}
if (signals.some(s => s.type === 'suicidal_ideation')) {
resources.push({
name: 'Crisis Text Line',
description: 'Text support with trained counselor',
action: 'sms:741741&body=HELLO',
priority: 2
});
}
return resources.sort((a, b) => a.priority - b.priority);
}---
Pattern 2: Location-Based Resources
Show local resources based on user location:
interface LocalResource {
name: string;
phone: string;
address: string;
hours: string;
services: string[];
distance?: number;
}
async function getLocalResources(
zipCode: string,
serviceType: 'crisis' | 'therapy' | 'substance_abuse'
): Promise<LocalResource[]> {
// Use SAMHSA Treatment Locator API
const response = await fetch(
`https://findtreatment.samhsa.gov/locator/api/v1/facilities?zip=${zipCode}&type=${serviceType}`
);
const facilities = await response.json();
return facilities.map(f => ({
name: f.name,
phone: f.phone,
address: f.address,
hours: f.hours,
services: f.services,
distance: f.distance
}));
}SAMHSA API: https://findtreatment.samhsa.gov/locator/api
---
Pattern 3: Multi-Channel Support
Allow users to choose their preferred contact method:
interface ContactMethod {
type: 'phone' | 'text' | 'chat' | 'email';
label: string;
action: string;
responseTime: string;
}
const crisis988Channels: ContactMethod[] = [
{
type: 'phone',
label: 'Call 988',
action: 'tel:988',
responseTime: '<1 minute'
},
{
type: 'text',
label: 'Text 988',
action: 'sms:988',
responseTime: '<5 minutes'
},
{
type: 'chat',
label: 'Chat Online',
action: 'https://988lifeline.org/chat',
responseTime: '<5 minutes'
}
];
// Display as user preference
function ResourceModal({ channels }: { channels: ContactMethod[] }) {
return (
<div>
<h2>How would you like to connect?</h2>
{channels.map(channel => (
<button key={channel.type} onClick={() => window.location.href = channel.action}>
{channel.label}
<span className="response-time">{channel.responseTime}</span>
</button>
))}
</div>
);
}---
Pattern 4: Resource Tracking
Track which resources users engage with:
interface ResourceEngagement {
userId: string;
resourceName: string;
contactMethod: 'phone' | 'text' | 'chat';
timestamp: Date;
fromDetection?: string; // Detection ID if from crisis detection
}
async function trackResourceEngagement(
userId: string,
resource: Resource,
detectionId?: string
): Promise<void> {
await db.resource_engagement.insert({
user_id: userId,
resource_name: resource.name,
contact_method: resource.type,
timestamp: new Date(),
from_detection: detectionId
});
// Update crisis detection record
if (detectionId) {
await db.crisis_detections.update(detectionId, {
resource_engaged: true,
resource_name: resource.name,
engaged_at: new Date()
});
}
}Benefits:
- Measure resource effectiveness
- Identify which resources users prefer
- Correlate resource use with outcomes
---
Pattern 5: Offline Support (For App Downtime)
Cache resources locally for offline access:
// Service Worker caching
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('crisis-resources-v1').then(cache => {
return cache.addAll([
'/crisis-resources.html',
'/crisis-resources.json'
]);
})
);
});
// Static crisis resources page (always available)
const offlineResources = `
<!DOCTYPE html>
<html>
<head>
<title>Crisis Resources</title>
</head>
<body>
<h1>🆘 Crisis Resources Available 24/7</h1>
<section>
<h2>988 Suicide & Crisis Lifeline</h2>
<a href="tel:988" class="primary-action">Call 988</a>
<p>Free, confidential support 24/7</p>
</section>
<section>
<h2>Crisis Text Line</h2>
<a href="sms:741741&body=HELLO">Text HELLO to 741741</a>
<p>Text support with trained counselor</p>
</section>
<!-- More resources -->
</body>
</html>
`;---
Mobile App Integration
Deep Linking
Allow direct app-to-app communication:
// iOS
const iosDeepLinks = {
phone: 'tel:988',
messages: 'sms:741741&body=HELLO',
safari: 'https://988lifeline.org/chat'
};
// Android
const androidDeepLinks = {
phone: 'tel:988',
messages: 'sms:741741?body=HELLO',
browser: 'https://988lifeline.org/chat'
};
// React Native
import { Linking } from 'react-native';
function callCrisisLine() {
Linking.openURL('tel:988');
}
function textCrisisLine() {
const url = Platform.OS === 'ios'
? 'sms:741741&body=HELLO'
: 'sms:741741?body=HELLO';
Linking.openURL(url);
}---
Push Notifications
Proactive check-ins for high-risk users:
// Schedule check-in notification
async function scheduleCheckIn(userId: string, hoursFromNow: number): Promise<void> {
await scheduleNotification({
userId,
title: 'Quick Check-In',
body: 'How are you feeling today? We\'re here if you need support.',
scheduledFor: new Date(Date.now() + hoursFromNow * 60 * 60 * 1000),
actions: [
{ id: 'talk', title: 'I\'d like to talk' },
{ id: 'resources', title: 'Show resources' },
{ id: 'dismiss', title: 'I\'m doing okay' }
]
});
}
// Handle notification response
notificationHandler.on('action', async (action, notification) => {
switch (action.id) {
case 'talk':
// Connect to crisis counselor
await connectToCounselor(notification.userId);
break;
case 'resources':
// Show crisis resources
showCrisisResources(notification.userId);
break;
case 'dismiss':
// Log as positive check-in
await logCheckIn(notification.userId, 'positive');
break;
}
});---
Resource Effectiveness Metrics
Track outcomes to improve resource recommendations:
-- Resource engagement tracking
CREATE TABLE resource_engagements (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
resource_name VARCHAR(255),
contact_method VARCHAR(50),
engaged_at TIMESTAMP,
from_detection_id UUID, -- Link to crisis detection
-- Outcome tracking
followed_up BOOLEAN, -- Did user follow up with counselor?
helpful_rating INTEGER, -- 1-5 scale
outcome_notes TEXT
);
-- Query: Which resources are most effective?
SELECT
resource_name,
contact_method,
COUNT(*) as engagements,
AVG(helpful_rating) as avg_rating,
SUM(CASE WHEN followed_up THEN 1 ELSE 0 END) as follow_ups
FROM resource_engagements
WHERE engaged_at > NOW() - INTERVAL '30 days'
GROUP BY resource_name, contact_method
ORDER BY avg_rating DESC, engagements DESC;---
Accessibility Considerations
Language Support
const resources988ByLanguage = {
en: {
phone: '988',
description: 'Free, confidential support 24/7'
},
es: {
phone: '988',
instructions: 'Press 2 for Spanish',
description: 'Apoyo gratuito y confidencial 24/7'
},
zh: {
// Chinese language resources
phone: '1-800-273-8255', // Alternate number with Chinese support
description: '免费、保密的支持 24/7'
}
};Disability Access
- Screen reader compatible: All resources have alt text
- TTY/TDD: 1-800-799-4889 (Deaf/Hard of Hearing)
- Video relay: Available through 988 website
---
Privacy & Security
Resource engagement is PHI:
// Encrypt resource engagement data
const encryptedEngagement = await encrypt({
userId: hashUserId(user.id), // Hash, not plain ID
resourceName: resource.name,
timestamp: Date.now()
}, process.env.CRISIS_DATA_KEY);
// Log access
await logAccess({
resourceId: resource.id,
accessedBy: 'system',
accessType: 'engagement_tracking',
timestamp: Date.now()
});
// Auto-delete after 30 days
await scheduleDelete({
table: 'resource_engagements',
recordId: engagement.id,
deleteAt: Date.now() + (30 * 24 * 60 * 60 * 1000)
});---
Testing Resource Integration
describe('Crisis Resources', () => {
it('shows 988 for all crisis severities', () => {
const resources = getRelevantResources([
{ type: 'suicidal_ideation', confidence: 0.9 }
]);
expect(resources[0].phone).toBe('988');
});
it('includes SAMHSA for substance relapse', () => {
const resources = getRelevantResources([
{ type: 'substance_relapse', confidence: 0.8 }
]);
expect(resources.some(r => r.name.includes('SAMHSA'))).toBe(true);
});
it('tracks resource engagement', async () => {
await trackResourceEngagement('user-123', {
name: '988 Lifeline',
type: 'phone'
});
const engagements = await db.resource_engagements.find({ user_id: 'user-123' });
expect(engagements.length).toBe(1);
});
});---
Resources
Crisis Intervention Protocols
Evidence-based intervention strategies for mental health crisis response.
Overview
Critical Rule: AI detects, humans intervene.
Timeline: From detection to human contact should be <15 minutes for immediate crises.
---
Severity-Based Response Matrix
| Severity | Response Time | Actions | Human Involvement |
|---|---|---|---|
| Immediate | <5 minutes | 988 modal (blocking), notify on-call, SMS backup | Required immediately |
| High | <1 hour | Crisis resources, notify on-call, email backup | Required within 1 hour |
| Medium | <24 hours | In-app resources, add to review queue | Review next business day |
| Low | <72 hours | Supportive resources, add to queue | Review within 3 days |
| None | N/A | No action | No action |
---
Protocol 1: Immediate Danger (Suicidal Ideation)
Trigger: severity === 'immediate'
Automated Actions (run immediately):
async function handleImmediateCrisis(detection: CrisisDetection): Promise<void> {
// 1. Show 988 modal (blocking, user must acknowledge)
await show988Modal({
userId: detection.userId,
dismissible: false, // User must click "I understand"
resources: [
{ name: '988 Suicide & Crisis Lifeline', action: 'tel:988' },
{ name: 'Crisis Text Line', action: 'sms:741741', message: 'HELLO' },
{ name: 'Chat with counselor', action: 'https://988lifeline.org/chat' }
]
});
// 2. Notify on-call crisis counselor (push notification)
await notifyOnCallCounselor({
userId: detection.userId,
severity: 'immediate',
text: detection.text,
signals: detection.signals,
requiresResponse: 'immediate',
escalationTime: Date.now()
});
// 3. SMS backup if no response in 5 minutes
setTimeout(async () => {
const responded = await checkCounselorResponse(detection.id);
if (!responded) {
await sendSMSToBackupCounselor({
message: `IMMEDIATE: ${detection.userId} crisis detected. No primary response in 5 min.`,
detectionId: detection.id
});
}
}, 5 * 60 * 1000);
// 4. Log for audit
await logCrisisEvent({
detectionId: detection.id,
severity: 'immediate',
actions: ['988_modal_shown', 'on_call_notified'],
timestamp: Date.now()
});
}Human Response (counselor script):
1. Immediate outreach (within 5 minutes):
- "Hi [name], I'm [counselor name], a crisis counselor. I saw your recent message and I'm here to help. Are you safe right now?"
2. Assess immediate danger:
- "Do you have a plan to hurt yourself?"
- "Are you alone right now?"
- "Do you have access to means (pills, weapons)?"
3. Safety planning:
- If danger: "Let's call 988 together right now."
- If no immediate danger: "Can we create a safety plan together?"
4. Follow-up:
- Schedule check-in within 24 hours
- Connect to ongoing therapy resources
---
Protocol 2: High Risk (Self-Harm, Suicidal Thoughts)
Trigger: severity === 'high'
Automated Actions:
async function handleHighRisk(detection: CrisisDetection): Promise<void> {
// 1. Show crisis resources (dismissible modal)
await showCrisisResourcesModal({
userId: detection.userId,
dismissible: true,
resources: [
{ name: '988 Suicide & Crisis Lifeline', action: 'tel:988' },
{ name: 'Crisis Text Line', action: 'sms:741741' },
{ name: 'Chat Now', action: 'https://988lifeline.org/chat' },
{ name: 'Find Therapist', action: '/find-therapist' }
],
message: 'We noticed you might be going through a tough time. Here are resources available 24/7.'
});
// 2. Notify on-call counselor (email + push)
await notifyOnCallCounselor({
severity: 'high',
requiresResponse: '1 hour',
channels: ['email', 'push']
});
// 3. Add to urgent review queue
await addToReviewQueue({
priority: 'urgent',
reviewBy: Date.now() + (1 * 60 * 60 * 1000) // 1 hour
});
}Human Response (within 1 hour):
1. Empathetic outreach:
- "Hi [name], I'm checking in because I care about how you're doing. How are you feeling right now?"
2. Non-judgmental listening:
- Validate feelings: "It sounds like you're going through a really difficult time."
- Ask open-ended questions: "What's been happening that's making you feel this way?"
3. Risk assessment:
- "Have you thought about hurting yourself?"
- "What's stopped you so far?" (identify protective factors)
4. Resource connection:
- Offer to help schedule therapy appointment
- Provide crisis line info if not already provided
- Create safety plan if appropriate
---
Protocol 3: Medium Risk (Substance Relapse)
Trigger: severity === 'medium'
Automated Actions:
async function handleMediumRisk(detection: CrisisDetection): Promise<void> {
// 1. Show in-app resources (non-blocking)
await showInAppResources({
userId: detection.userId,
type: 'supportive',
resources: [
{ name: 'Talk to Sponsor', action: '/contacts/sponsor' },
{ name: 'Find a Meeting', action: '/meetings/nearby' },
{ name: 'Call Support Line', action: 'tel:1-800-662-4357' }, // SAMHSA
{ name: 'Coping Strategies', action: '/resources/coping' }
]
});
// 2. Flag for review (next business day)
await addToReviewQueue({
priority: 'high',
reviewBy: getNextBusinessDay()
});
// 3. Suggest self-help actions
await suggestActions({
userId: detection.userId,
actions: [
'Reach out to your sponsor',
'Attend a meeting today',
'Practice grounding techniques',
'Call a supportive friend'
]
});
}Human Response (within 24 hours):
1. Supportive check-in:
- "Hi [name], how are you doing today?"
2. Assess situation:
- "I saw you mentioned [relapse/cravings]. Want to talk about it?"
- "What triggered this?"
3. Action planning:
- "What's one thing you can do today to support your recovery?"
- "Have you been to a meeting recently?"
- "Is your sponsor aware?"
4. Resource connection:
- Remind about support group schedule
- Share coping strategies
- Schedule follow-up
---
Protocol 4: Low Risk (General Distress)
Trigger: severity === 'low'
Automated Actions:
async function handleLowRisk(detection: CrisisDetection): Promise<void> {
// 1. Offer supportive resources (subtle, non-intrusive)
await showResources({
userId: detection.userId,
placement: 'sidebar', // Not blocking
resources: [
{ name: 'Self-Care Tips', action: '/resources/self-care' },
{ name: 'Mindfulness Exercises', action: '/resources/mindfulness' },
{ name: 'Community Support', action: '/community' }
]
});
// 2. Add to review queue (normal priority)
await addToReviewQueue({
priority: 'normal',
reviewBy: Date.now() + (3 * 24 * 60 * 60 * 1000) // 3 days
});
}Human Response (within 72 hours):
1. Casual check-in:
- "Hey [name], just checking in. How's your week going?"
2. Light conversation:
- Listen without pushing for crisis disclosure
- Offer general support
3. Resource awareness:
- "Just wanted to remind you we have [resource] available if you ever need it."
---
Special Protocol: Substance Relapse
Unique considerations: Shame, guilt, fear of judgment
DO:
- ✅ Normalize relapse as part of recovery journey
- ✅ Focus on getting back on track, not dwelling on slip
- ✅ Connect to sponsor/support group immediately
- ✅ Celebrate previous sobriety streak (e.g., "6 months is amazing!")
DON'T:
- ❌ Express disappointment or judgment
- ❌ Ask "why did you do it?" (increases shame)
- ❌ Minimize: "It's just one slip"
Script:
"Hey [name], I saw you mentioned using again. First, I want you to know
that reaching out takes courage. Relapse is a part of recovery for many
people, and it doesn't erase the progress you've made.
[6 months sober] is a huge accomplishment. That shows you have the
strength to do this.
What can we do right now to support you? Would it help to:
- Call your sponsor?
- Find a meeting today?
- Talk through what triggered this?
You're not alone in this."---
Escalation Chain
If primary on-call counselor doesn't respond:
0-5 min: Primary on-call notified (push notification)
5-10 min: Backup on-call notified (SMS)
10-15 min: Clinical supervisor notified (phone call)
15+ min: Emergency protocol (suggest user call 988 directly)Implementation:
async function escalateIfNoResponse(detection: CrisisDetection): Promise<void> {
const escalationSteps = [
{ delay: 0, action: () => notifyPrimaryOnCall(detection) },
{ delay: 5 * 60 * 1000, action: () => notifyBackupOnCall(detection) },
{ delay: 10 * 60 * 1000, action: () => notifySupervisor(detection) },
{ delay: 15 * 60 * 1000, action: () => showEmergencyModal(detection.userId) }
];
for (const step of escalationSteps) {
setTimeout(async () => {
const responded = await checkResponse(detection.id);
if (!responded) {
await step.action();
}
}, step.delay);
}
}---
Documentation Requirements
For every crisis intervention:
1. Detection Record:
- Timestamp
- Detected signals
- Severity level
- Confidence score
2. Action Log:
- Resources shown
- Who was notified
- When they responded
- What actions were taken
3. Outcome:
- User status after intervention
- Follow-up scheduled?
- Escalation needed?
4. Compliance:
- Access logged (who viewed crisis content)
- Auto-delete scheduled (30 days)
- Encryption verified
Sample Documentation:
{
"detection_id": "crisis-123",
"user_id": "user-456",
"detected_at": "2024-01-15T14:32:00Z",
"severity": "high",
"signals": ["suicidal_ideation"],
"confidence": 0.92,
"actions_taken": [
{
"action": "988_modal_shown",
"timestamp": "2024-01-15T14:32:01Z"
},
{
"action": "on_call_notified",
"timestamp": "2024-01-15T14:32:02Z",
"counselor_id": "counselor-789"
},
{
"action": "counselor_responded",
"timestamp": "2024-01-15T14:34:15Z",
"response_time_seconds": 133
}
],
"outcome": {
"user_safe": true,
"follow_up_scheduled": "2024-01-16T10:00:00Z",
"notes": "User connected with counselor, safety plan created"
},
"auto_delete_at": "2024-02-14T14:32:00Z"
}---
Training Materials
For crisis counselors:
1. Understanding AI Detection:
- How the system works (multi-signal detection)
- What triggers each severity level
- False positive handling
2. Response Protocols:
- Scripts for each severity level
- Escalation procedures
- Documentation requirements
3. Crisis Resources:
- 988 Suicide & Crisis Lifeline
- Crisis Text Line (741741)
- SAMHSA National Helpline (1-800-662-4357)
- Local emergency resources
4. Self-Care:
- Secondary trauma awareness
- Counselor support resources
- Burnout prevention
---
Quality Assurance
Regular review:
- ✅ Weekly: Review all immediate/high severity interventions
- ✅ Monthly: Analyze false positive/negative rates
- ✅ Quarterly: Update keyword patterns based on missed cases
- ✅ Annually: Full protocol review with clinical team
Metrics to track:
- Response time (goal: <5 min for immediate)
- False positive rate (goal: <10%)
- False negative rate (goal: <5%)
- User outcome (did intervention help?)
- Counselor satisfaction
---
Legal & Ethical Considerations
Informed Consent:
- Users must consent to crisis monitoring
- Clear explanation of how detection works
- Opt-out option (with strong warning)
Mandatory Reporting:
- Imminent danger to self: Report to emergency services
- Imminent danger to others: Report to authorities
- Child abuse: Report to authorities (per jurisdiction)
Confidentiality:
- Crisis content is PHI (HIPAA applies)
- Only licensed professionals can access
- Access is logged and audited
- Auto-deleted after retention period
Liability:
- AI is assistive tool, not replacement for professional judgment
- Counselors make final decisions
- Clear disclaimers in app
- Professional liability insurance required
---
Resources
Mental Health NLP Models
Comprehensive guide to NLP models for mental health crisis detection.
Model Comparison (2024)
| Model | Accuracy | Latency | Use Case | License |
|---|---|---|---|---|
| Mental-BERT | 89% | 50ms | Depression, anxiety detection | MIT |
| MentalRoBERTa | 87% | 40ms | Suicidal ideation | MIT |
| GPT-4 (few-shot) | 92% | 200ms | General crisis detection | Commercial |
| Claude 3.5 Sonnet | 91% | 150ms | Contextual analysis | Commercial |
| Custom fine-tuned BERT | 90%+ | 60ms | Domain-specific (e.g., addiction) | Depends |
---
Using Mental-BERT
Installation
npm install @huggingface/transformersClassification
import { pipeline } from '@huggingface/transformers';
const detector = await pipeline(
'text-classification',
'mental/mental-bert-base-uncased'
);
const result = await detector("I don't want to live anymore", {
top_k: 3
});
console.log(result);
// [
// { label: 'suicidal_ideation', score: 0.92 },
// { label: 'severe_depression', score: 0.78 },
// { label: 'self_harm', score: 0.45 }
// ]---
Fine-Tuning for Your Domain
Dataset Format
[
{
"text": "I relapsed today after 6 months sober",
"label": "substance_relapse",
"severity": "medium"
},
{
"text": "I can't do this anymore, I want to end it",
"label": "suicidal_ideation",
"severity": "high"
},
{
"text": "Had a stressful day at work",
"label": "normal_distress",
"severity": "none"
}
]Training Script
from transformers import AutoModelForSequenceClassification, Trainer, TrainingArguments
from datasets import load_dataset
# Load pre-trained model
model = AutoModelForSequenceClassification.from_pretrained(
"mental/mental-bert-base-uncased",
num_labels=4 # suicidal, self_harm, relapse, safe
)
# Load your dataset
dataset = load_dataset('json', data_files='crisis_training_data.json')
# Training arguments
training_args = TrainingArguments(
output_dir="./crisis-model",
num_train_epochs=3,
per_device_train_batch_size=16,
per_device_eval_batch_size=64,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True
)
# Train
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset['train'],
eval_dataset=dataset['test']
)
trainer.train()---
Evaluation Metrics
Confusion Matrix
Predicted
Crisis Safe
Actual Crisis 92 8 (Recall: 92%)
Safe 5 95 (Precision: 95%)Key Metrics:
- Recall (Sensitivity): 92% - How many actual crises are detected
- Precision: 95% - How many detections are actual crises
- F1 Score: 93.5% - Harmonic mean
Which matters more?
- Crisis detection: Maximize recall (don't miss crises, accept false positives)
- Spam detection: Maximize precision (avoid false alarms)
---
Production Deployment
Caching for Performance
import { pipeline } from '@huggingface/transformers';
class CachedDetector {
private model: any;
private cache = new Map<string, any>();
async init() {
this.model = await pipeline(
'text-classification',
'mental/mental-bert-base-uncased'
);
}
async detect(text: string): Promise<any> {
// Cache by hash
const hash = hashText(text);
if (this.cache.has(hash)) {
return this.cache.get(hash);
}
const result = await this.model(text);
this.cache.set(hash, result);
return result;
}
}---
Resources
#!/usr/bin/env node
/**
* Crisis Detection System
*
* Real-time mental health crisis detection using multiple signals.
*
* Usage: npx tsx crisis_detector.ts [text]
*
* Examples:
* npx tsx crisis_detector.ts "I don't want to be here anymore"
* npx tsx crisis_detector.ts "Had a tough day at work"
*
* Signals:
* - NLP model classification
* - Keyword pattern matching
* - Sentiment + context analysis
*/
interface CrisisSignal {
type: 'suicidal_ideation' | 'self_harm' | 'substance_relapse' | 'severe_distress';
confidence: number;
evidence: string[];
source: 'nlp' | 'keywords' | 'sentiment' | 'context';
}
interface CrisisDetection {
isCrisis: boolean;
severity: 'none' | 'low' | 'medium' | 'high' | 'immediate';
signals: CrisisSignal[];
confidence: number;
recommendations: string[];
}
// Crisis keywords (evidence-based patterns)
const CRISIS_PATTERNS = {
suicidal_ideation: [
/\b(kill|end|take)\s+(my|own)\s+life\b/i,
/\bsuicide\b/i,
/\bdon'?t\s+want\s+to\s+(live|be here|exist)\b/i,
/\bbetter off dead\b/i,
/\bno\s+reason\s+to\s+(live|continue|go on)\b/i,
/\bcan'?t\s+(take|do)\s+this\s+anymore\b/i
],
self_harm: [
/\b(cut|cutting|hurt|hurting)\s+(myself|me)\b/i,
/\bself[- ]harm\b/i,
/\bburning\s+myself\b/i
],
substance_relapse: [
/\b(relapsed|used|drank)\s+(again|today|yesterday)\b/i,
/\bback on\s+(drugs|alcohol|using)\b/i,
/\bcouldn'?t\s+stay\s+sober\b/i,
/\b(cravings?|urges?)\s+(are\s+)?too\s+strong\b/i
],
hopelessness: [
/\bno\s+(hope|point|reason|future)\b/i,
/\bnothing\s+(matters|helps|works)\b/i,
/\bcan'?t\s+see\s+a\s+way\s+out\b/i,
/\ball\s+alone\b/i,
/\bnobody\s+(cares|understands)\b/i
]
};
// Protective factors (reduce crisis severity)
const PROTECTIVE_PATTERNS = [
/\b(therapist|counselor|support\s+group)\b/i,
/\b(reaching\s+out|asking\s+for\s+help)\b/i,
/\b(called|talked\s+to)\s+(friend|family|sponsor)\b/i,
/\bsafety\s+plan\b/i
];
class CrisisDetector {
/**
* Detect crisis signals in text
*/
detect(text: string): CrisisDetection {
const signals: CrisisSignal[] = [];
// Signal 1: Keyword matching
const keywordSignals = this.detectKeywords(text);
signals.push(...keywordSignals);
// Signal 2: Sentiment analysis (simplified)
const sentimentSignal = this.analyzeSentiment(text);
if (sentimentSignal) {
signals.push(sentimentSignal);
}
// Signal 3: Context analysis
const contextSignal = this.analyzeContext(text);
if (contextSignal) {
signals.push(contextSignal);
}
// Check for protective factors
const hasProtection = this.hasProtectiveFactors(text);
// Calculate overall severity
const severity = this.calculateSeverity(signals, hasProtection);
const confidence = this.calculateConfidence(signals);
// Generate recommendations
const recommendations = this.generateRecommendations(severity, signals);
return {
isCrisis: severity !== 'none',
severity,
signals,
confidence,
recommendations
};
}
private detectKeywords(text: string): CrisisSignal[] {
const signals: CrisisSignal[] = [];
for (const [type, patterns] of Object.entries(CRISIS_PATTERNS)) {
const matches: string[] = [];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
matches.push(match[0]);
}
}
if (matches.length > 0) {
signals.push({
type: type as CrisisSignal['type'],
confidence: Math.min(0.6 + (matches.length * 0.1), 0.9),
evidence: matches,
source: 'keywords'
});
}
}
return signals;
}
private analyzeSentiment(text: string): CrisisSignal | null {
// Simplified sentiment analysis
const negativeWords = ['hate', 'pain', 'hurt', 'sad', 'miserable', 'awful', 'terrible'];
const negativeCount = negativeWords.filter(word =>
new RegExp(`\\b${word}\\b`, 'i').test(text)
).length;
const totalWords = text.split(/\s+/).length;
const negativeRatio = negativeCount / totalWords;
if (negativeRatio > 0.2) {
return {
type: 'severe_distress',
confidence: Math.min(negativeRatio * 3, 0.8),
evidence: [`High negative sentiment: ${(negativeRatio * 100).toFixed(1)}%`],
source: 'sentiment'
};
}
return null;
}
private analyzeContext(text: string): CrisisSignal | null {
// Check for hopelessness + negative sentiment
const hasHopelessness = CRISIS_PATTERNS.hopelessness.some(pattern =>
pattern.test(text)
);
const hasNegativeFuture = /\b(never|won'?t|can'?t)\s+(get|be)\s+better\b/i.test(text);
if (hasHopelessness && hasNegativeFuture) {
return {
type: 'severe_distress',
confidence: 0.75,
evidence: ['Hopelessness + negative future outlook'],
source: 'context'
};
}
return null;
}
private hasProtectiveFactors(text: string): boolean {
return PROTECTIVE_PATTERNS.some(pattern => pattern.test(text));
}
private calculateSeverity(
signals: CrisisSignal[],
hasProtection: boolean
): CrisisDetection['severity'] {
if (signals.length === 0) {
return 'none';
}
// Check for high-risk signals
const hasSuicidalIdeation = signals.some(s =>
s.type === 'suicidal_ideation' && s.confidence > 0.7
);
const hasSelfHarm = signals.some(s =>
s.type === 'self_harm' && s.confidence > 0.6
);
const maxConfidence = Math.max(...signals.map(s => s.confidence));
if (hasSuicidalIdeation && !hasProtection) {
return maxConfidence > 0.85 ? 'immediate' : 'high';
}
if (hasSelfHarm || hasSuicidalIdeation) {
return 'high';
}
if (signals.some(s => s.type === 'substance_relapse')) {
return 'medium';
}
if (signals.some(s => s.type === 'severe_distress')) {
return hasProtection ? 'low' : 'medium';
}
return 'low';
}
private calculateConfidence(signals: CrisisSignal[]): number {
if (signals.length === 0) return 0;
// Average confidence across all signals
const avgConfidence = signals.reduce((sum, s) => sum + s.confidence, 0) / signals.length;
// Boost confidence if multiple signals agree
const uniqueTypes = new Set(signals.map(s => s.type));
const agreementBoost = signals.length > 1 ? 0.1 : 0;
return Math.min(avgConfidence + agreementBoost, 1.0);
}
private generateRecommendations(
severity: CrisisDetection['severity'],
signals: CrisisSignal[]
): string[] {
const recommendations: string[] = [];
switch (severity) {
case 'immediate':
case 'high':
recommendations.push('IMMEDIATE: Show 988 Suicide & Crisis Lifeline modal');
recommendations.push('IMMEDIATE: Notify on-call crisis counselor');
recommendations.push('IMMEDIATE: Log for urgent review');
recommendations.push('Provide Crisis Text Line: 741741');
break;
case 'medium':
recommendations.push('Show crisis resources in-app');
recommendations.push('Flag for counselor review within 24 hours');
recommendations.push('Suggest self-help coping strategies');
break;
case 'low':
recommendations.push('Add to review queue (normal priority)');
recommendations.push('Offer supportive resources');
break;
case 'none':
recommendations.push('No action needed');
break;
}
// Add signal-specific recommendations
if (signals.some(s => s.type === 'substance_relapse')) {
recommendations.push('Connect user to sponsor contact');
recommendations.push('Suggest attending a meeting');
}
if (signals.some(s => s.type === 'self_harm')) {
recommendations.push('Provide grounding techniques');
recommendations.push('Suggest harm reduction resources');
}
return recommendations;
}
/**
* Format detection result for display
*/
format(detection: CrisisDetection): string {
const severityEmoji = {
none: '✅',
low: '💛',
medium: '🟠',
high: '🔴',
immediate: '🚨'
};
let output = `\n${severityEmoji[detection.severity]} Crisis Detection Result\n\n`;
output += `Severity: ${detection.severity.toUpperCase()}\n`;
output += `Confidence: ${(detection.confidence * 100).toFixed(1)}%\n`;
output += `Is Crisis: ${detection.isCrisis ? 'YES' : 'NO'}\n\n`;
if (detection.signals.length > 0) {
output += 'Detected Signals:\n';
detection.signals.forEach(signal => {
output += `\n • ${signal.type.replace(/_/g, ' ').toUpperCase()}\n`;
output += ` Confidence: ${(signal.confidence * 100).toFixed(1)}%\n`;
output += ` Source: ${signal.source}\n`;
output += ` Evidence: ${signal.evidence.join(', ')}\n`;
});
}
output += '\n─'.repeat(80) + '\n';
output += '\nRecommended Actions:\n';
detection.recommendations.forEach(rec => {
output += `\n ${rec}`;
});
output += '\n\n🔒 Privacy Note: All crisis detections must be:\n';
output += ' • Stored encrypted\n';
output += ' • Access-logged\n';
output += ' • Auto-deleted after 30 days\n';
output += ' • Reviewed by licensed professionals only\n';
output += '\n';
return output;
}
}
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: npx tsx crisis_detector.ts [text]');
console.log('\nExamples:');
console.log(' npx tsx crisis_detector.ts "I don\'t want to be here anymore"');
console.log(' npx tsx crisis_detector.ts "Had a tough day at work"');
console.log(' npx tsx crisis_detector.ts "I relapsed today, feeling awful"');
console.log('\n⚠️ This is a detection tool, NOT a substitute for professional help.');
console.log('Always consult licensed mental health professionals.');
process.exit(1);
}
const text = args.join(' ');
console.log('\n📝 Analyzing text:');
console.log(`"${text}"\n`);
const detector = new CrisisDetector();
const result = detector.detect(text);
console.log(detector.format(result));
// Show crisis resources for any detected crisis
if (result.isCrisis) {
console.log('═'.repeat(80));
console.log('\n🆘 CRISIS RESOURCES AVAILABLE 24/7:\n');
console.log(' • 988 Suicide & Crisis Lifeline');
console.log(' Phone: 988');
console.log(' Chat: https://988lifeline.org/chat');
console.log('\n • Crisis Text Line');
console.log(' Text "HELLO" to 741741');
console.log('\n • SAMHSA National Helpline (Substance Abuse)');
console.log(' Phone: 1-800-662-4357');
console.log('\n═'.repeat(80) + '\n');
}
}
export { CrisisDetector, CrisisDetection, CrisisSignal };
#!/usr/bin/env node
/**
* Crisis Detection Model Evaluator
*
* Evaluates crisis detection accuracy using test cases with ground truth labels.
*
* Usage: npx tsx model_evaluator.ts [test-cases.json]
*
* Test Case Format:
* [
* {
* "text": "I don't want to be here anymore",
* "expected_severity": "high",
* "expected_signals": ["suicidal_ideation"]
* }
* ]
*
* Metrics:
* - Precision: Of all detected crises, how many were actual crises?
* - Recall: Of all actual crises, how many did we detect?
* - F1 Score: Harmonic mean of precision and recall
* - Confusion Matrix: True/false positives/negatives
*/
import { CrisisDetector, CrisisDetection, CrisisSignal } from './crisis_detector';
import * as fs from 'fs';
interface TestCase {
text: string;
expected_severity: 'none' | 'low' | 'medium' | 'high' | 'immediate';
expected_signals: string[];
description?: string;
}
interface EvaluationResult {
testCase: TestCase;
detection: CrisisDetection;
severityMatch: boolean;
signalsMatch: boolean;
truePositive: boolean;
falsePositive: boolean;
trueNegative: boolean;
falseNegative: boolean;
}
interface Metrics {
accuracy: number;
precision: number;
recall: number;
f1Score: number;
confusionMatrix: {
truePositives: number;
falsePositives: number;
trueNegatives: number;
falseNegatives: number;
};
severityAccuracy: number;
signalAccuracy: number;
}
class ModelEvaluator {
private detector: CrisisDetector;
constructor() {
this.detector = new CrisisDetector();
}
/**
* Load test cases from JSON file
*/
loadTestCases(filePath: string): TestCase[] {
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}
/**
* Evaluate model on test cases
*/
evaluate(testCases: TestCase[]): EvaluationResult[] {
const results: EvaluationResult[] = [];
for (const testCase of testCases) {
const detection = this.detector.detect(testCase.text);
const severityMatch = detection.severity === testCase.expected_severity;
// Check if detected signals match expected signals
const detectedTypes = new Set(detection.signals.map(s => s.type));
const expectedTypes = new Set(testCase.expected_signals);
const signalsMatch = this.setsEqual(detectedTypes, expectedTypes);
// Classify result for confusion matrix
const actualCrisis = testCase.expected_severity !== 'none';
const detectedCrisis = detection.isCrisis;
const truePositive = actualCrisis && detectedCrisis;
const falsePositive = !actualCrisis && detectedCrisis;
const trueNegative = !actualCrisis && !detectedCrisis;
const falseNegative = actualCrisis && !detectedCrisis;
results.push({
testCase,
detection,
severityMatch,
signalsMatch,
truePositive,
falsePositive,
trueNegative,
falseNegative
});
}
return results;
}
/**
* Calculate metrics from evaluation results
*/
calculateMetrics(results: EvaluationResult[]): Metrics {
const tp = results.filter(r => r.truePositive).length;
const fp = results.filter(r => r.falsePositive).length;
const tn = results.filter(r => r.trueNegative).length;
const fn = results.filter(r => r.falseNegative).length;
const accuracy = (tp + tn) / results.length;
const precision = tp > 0 ? tp / (tp + fp) : 0;
const recall = tp > 0 ? tp / (tp + fn) : 0;
const f1Score = precision + recall > 0
? 2 * (precision * recall) / (precision + recall)
: 0;
const severityMatches = results.filter(r => r.severityMatch).length;
const severityAccuracy = severityMatches / results.length;
const signalMatches = results.filter(r => r.signalsMatch).length;
const signalAccuracy = signalMatches / results.length;
return {
accuracy,
precision,
recall,
f1Score,
confusionMatrix: {
truePositives: tp,
falsePositives: fp,
trueNegatives: tn,
falseNegatives: fn
},
severityAccuracy,
signalAccuracy
};
}
/**
* Generate evaluation report
*/
generateReport(results: EvaluationResult[], metrics: Metrics): string {
let report = '\n📊 Crisis Detection Model Evaluation\n';
report += '═'.repeat(80) + '\n\n';
// Overall Metrics
report += '## Overall Metrics\n\n';
report += `Accuracy: ${(metrics.accuracy * 100).toFixed(1)}%\n`;
report += `Precision: ${(metrics.precision * 100).toFixed(1)}%\n`;
report += `Recall: ${(metrics.recall * 100).toFixed(1)}%\n`;
report += `F1 Score: ${(metrics.f1Score * 100).toFixed(1)}%\n\n`;
// Confusion Matrix
report += '## Confusion Matrix\n\n';
report += ' Predicted\n';
report += ' Crisis Safe\n';
report += `Actual Crisis ${metrics.confusionMatrix.truePositives.toString().padStart(3)} ${metrics.confusionMatrix.falseNegatives.toString().padStart(3)}\n`;
report += ` Safe ${metrics.confusionMatrix.falsePositives.toString().padStart(3)} ${metrics.confusionMatrix.trueNegatives.toString().padStart(3)}\n\n`;
// Detailed Metrics
report += '## Detailed Metrics\n\n';
report += `Severity Accuracy: ${(metrics.severityAccuracy * 100).toFixed(1)}%\n`;
report += `Signal Accuracy: ${(metrics.signalAccuracy * 100).toFixed(1)}%\n\n`;
// False Negatives (Most Critical)
report += '## ⚠️ False Negatives (Missed Crises)\n\n';
const falseNegatives = results.filter(r => r.falseNegative);
if (falseNegatives.length > 0) {
falseNegatives.forEach((result, i) => {
report += `${i + 1}. "${result.testCase.text}"\n`;
report += ` Expected: ${result.testCase.expected_severity} | Detected: ${result.detection.severity}\n`;
report += ` Expected Signals: ${result.testCase.expected_signals.join(', ')}\n`;
report += ` Detected Signals: ${result.detection.signals.map(s => s.type).join(', ') || 'none'}\n\n`;
});
} else {
report += 'None ✅\n\n';
}
// False Positives
report += '## 🚨 False Positives (False Alarms)\n\n';
const falsePositives = results.filter(r => r.falsePositive);
if (falsePositives.length > 0) {
falsePositives.forEach((result, i) => {
report += `${i + 1}. "${result.testCase.text}"\n`;
report += ` Expected: ${result.testCase.expected_severity} | Detected: ${result.detection.severity}\n`;
report += ` Confidence: ${(result.detection.confidence * 100).toFixed(1)}%\n\n`;
});
} else {
report += 'None ✅\n\n';
}
// Severity Breakdown
report += '## Severity Level Performance\n\n';
const severityLevels: Array<'none' | 'low' | 'medium' | 'high' | 'immediate'> =
['none', 'low', 'medium', 'high', 'immediate'];
severityLevels.forEach(level => {
const casesAtLevel = results.filter(r => r.testCase.expected_severity === level);
if (casesAtLevel.length > 0) {
const correctAtLevel = casesAtLevel.filter(r => r.severityMatch).length;
const accuracy = (correctAtLevel / casesAtLevel.length * 100).toFixed(1);
report += `${level.toUpperCase().padEnd(10)} ${correctAtLevel}/${casesAtLevel.length} (${accuracy}%)\n`;
}
});
report += '\n';
// Recommendations
report += '## 💡 Recommendations\n\n';
if (metrics.recall < 0.9) {
report += '• ⚠️ CRITICAL: Recall is below 90%. Missing real crises.\n';
report += ' → Lower detection threshold or add more keyword patterns\n\n';
}
if (metrics.precision < 0.7) {
report += '• False positive rate is high (>30%)\n';
report += ' → Increase confidence threshold or refine keyword patterns\n\n';
}
if (falseNegatives.length > 0) {
report += `• Review ${falseNegatives.length} false negative(s) and add missing patterns\n\n`;
}
if (metrics.severityAccuracy < 0.8) {
report += '• Severity classification needs improvement\n';
report += ' → Review severity calculation logic\n\n';
}
return report;
}
/**
* Helper: Check if two sets are equal
*/
private setsEqual<T>(set1: Set<T>, set2: Set<T>): boolean {
if (set1.size !== set2.size) return false;
for (const item of set1) {
if (!set2.has(item)) return false;
}
return true;
}
/**
* Export results to JSON
*/
exportResults(results: EvaluationResult[], outputPath: string): void {
const output = {
timestamp: new Date().toISOString(),
total_cases: results.length,
results: results.map(r => ({
text: r.testCase.text,
expected: {
severity: r.testCase.expected_severity,
signals: r.testCase.expected_signals
},
detected: {
severity: r.detection.severity,
signals: r.detection.signals.map(s => s.type),
confidence: r.detection.confidence
},
correct: r.severityMatch && r.signalsMatch
}))
};
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
console.log(`\n✅ Results exported to ${outputPath}`);
}
}
// Default test cases
const DEFAULT_TEST_CASES: TestCase[] = [
// Suicidal ideation - high severity
{
text: "I don't want to be here anymore",
expected_severity: 'high',
expected_signals: ['suicidal_ideation'],
description: 'Clear suicidal ideation'
},
{
text: "I'm thinking about ending it all",
expected_severity: 'high',
expected_signals: ['suicidal_ideation'],
description: 'Explicit suicidal thoughts'
},
{
text: "Everyone would be better off without me",
expected_severity: 'high',
expected_signals: ['suicidal_ideation'],
description: 'Suicidal ideation with perceived burden'
},
// Self-harm
{
text: "I cut myself last night",
expected_severity: 'high',
expected_signals: ['self_harm'],
description: 'Self-harm disclosure'
},
// Substance relapse
{
text: "I relapsed today after 6 months sober",
expected_severity: 'medium',
expected_signals: ['substance_relapse'],
description: 'Substance relapse'
},
{
text: "The cravings are too strong, I can't resist",
expected_severity: 'medium',
expected_signals: ['substance_relapse'],
description: 'Strong cravings indicating risk'
},
// Severe distress (no specific crisis)
{
text: "I feel so hopeless and alone, nothing matters anymore",
expected_severity: 'medium',
expected_signals: ['severe_distress'],
description: 'Severe distress with hopelessness'
},
// Normal distress - should NOT trigger crisis
{
text: "Had a tough day at work, feeling stressed",
expected_severity: 'none',
expected_signals: [],
description: 'Normal work stress'
},
{
text: "Feeling a bit down today",
expected_severity: 'none',
expected_signals: [],
description: 'Mild sadness'
},
{
text: "I'm tired of this project, it's taking forever",
expected_severity: 'none',
expected_signals: [],
description: 'Frustration (not crisis)'
},
// Edge cases
{
text: "I want to kill this bug in my code",
expected_severity: 'none',
expected_signals: [],
description: 'False positive test: coding language'
},
{
text: "This movie is killing me, so funny!",
expected_severity: 'none',
expected_signals: [],
description: 'False positive test: figurative language'
},
// With protective factors
{
text: "Having dark thoughts but I called my therapist and have a safety plan",
expected_severity: 'low',
expected_signals: ['severe_distress'],
description: 'Crisis with protective factors'
}
];
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
let testCases: TestCase[];
const evaluator = new ModelEvaluator();
if (args.length > 0) {
// Load test cases from file
const filePath = args[0];
console.log(`\n📂 Loading test cases from ${filePath}...`);
testCases = evaluator.loadTestCases(filePath);
} else {
// Use default test cases
console.log('\n📝 Using default test cases...');
testCases = DEFAULT_TEST_CASES;
}
console.log(`\n🧪 Evaluating model on ${testCases.length} test cases...\n`);
// Run evaluation
const results = evaluator.evaluate(testCases);
const metrics = evaluator.calculateMetrics(results);
// Generate and display report
const report = evaluator.generateReport(results, metrics);
console.log(report);
// Export results if requested
if (args.includes('--export')) {
const outputPath = args.includes('--output')
? args[args.indexOf('--output') + 1]
: 'evaluation-results.json';
evaluator.exportResults(results, outputPath);
}
// Exit with error code if recall is too low (missing real crises)
if (metrics.recall < 0.9) {
console.log('❌ CRITICAL: Recall below 90%. Model is missing real crises.\n');
process.exit(1);
}
console.log('✅ Evaluation complete.\n');
}
export { ModelEvaluator, TestCase, EvaluationResult, Metrics };