Compare commits
4 Commits
step35/200
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f30b0d962d | |||
| 26dc58faa7 | |||
|
|
572f14eda5 | ||
|
|
58fdd9cfef |
134
crisis/intake.py
Normal file
134
crisis/intake.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Intake submission handler for the-door.
|
||||
|
||||
Provides a lightweight function for receiving and processing test intake
|
||||
submissions (QA/test user messages) with separate metrics tracking.
|
||||
|
||||
Usage:
|
||||
from crisis.intake import handle_intake_submission
|
||||
|
||||
result = handle_intake_submission("This is a test message", test=True)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
from .gateway import check_crisis
|
||||
from .metrics import CrisisMetrics, SessionMetrics
|
||||
from .session_tracker import CrisisSessionTracker
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntakeResult:
|
||||
"""Result from processing an intake submission."""
|
||||
crisis_level: str
|
||||
score: float
|
||||
timmy_message: str
|
||||
show_overlay: bool
|
||||
provide_988: bool
|
||||
is_test: bool
|
||||
session_id: Optional[str] = None
|
||||
|
||||
|
||||
# Separate metrics namespace for test intake tracking
|
||||
_INTAKE_METRICS_DIR = Path.home() / ".the-door" / "intake-metrics"
|
||||
|
||||
|
||||
def handle_intake_submission(
|
||||
message: str,
|
||||
test: bool = True,
|
||||
session_id: Optional[str] = None,
|
||||
) -> IntakeResult:
|
||||
"""
|
||||
Handle an intake submission from a user (including test users).
|
||||
|
||||
Args:
|
||||
message: The user's message text.
|
||||
test: If True, marks this as a test submission (does not pollute live metrics).
|
||||
session_id: Optional session identifier for correlation.
|
||||
|
||||
Returns:
|
||||
IntakeResult with crisis assessment and response data.
|
||||
"""
|
||||
# Run crisis detection
|
||||
crisis_result = check_crisis(message)
|
||||
|
||||
# For test submissions, optionally log to separate test metrics
|
||||
if test:
|
||||
_log_test_intake(crisis_result, message)
|
||||
|
||||
return IntakeResult(
|
||||
crisis_level=crisis_result["level"],
|
||||
score=crisis_result["score"],
|
||||
timmy_message=crisis_result["timmy_message"],
|
||||
show_overlay=crisis_result["ui"]["show_overlay"],
|
||||
provide_988=crisis_result["ui"]["provide_988"],
|
||||
is_test=test,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
|
||||
def _log_test_intake(detection, message: str) -> None:
|
||||
"""
|
||||
Log test intake submissions to a separate metrics file.
|
||||
|
||||
These do not mix with production crisis statistics.
|
||||
|
||||
Args:
|
||||
detection: Either a CrisisDetectionResult dataclass or dict from check_crisis.
|
||||
message: The original user message.
|
||||
"""
|
||||
import time
|
||||
import json
|
||||
|
||||
# Accept both dataclass and dict
|
||||
if hasattr(detection, '__dataclass_fields__'):
|
||||
level = detection.level
|
||||
score = detection.score
|
||||
indicators = detection.indicators
|
||||
else:
|
||||
level = detection.get("level")
|
||||
score = detection.get("score")
|
||||
indicators = detection.get("indicators", [])
|
||||
|
||||
_INTAKE_METRICS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
today = time.strftime("%Y-%m-%d")
|
||||
filepath = _INTAKE_METRICS_DIR / f"test-intake-{today}.jsonl"
|
||||
|
||||
record = {
|
||||
"timestamp": time.time(),
|
||||
"level": level,
|
||||
"score": score,
|
||||
"message_preview": message[:80],
|
||||
"indicators": indicators,
|
||||
}
|
||||
|
||||
with open(filepath, "a") as f:
|
||||
f.write(json.dumps(record) + "\n")
|
||||
|
||||
|
||||
# ── Quick test interface ────────────────────────────────────────────
|
||||
|
||||
def _interactive():
|
||||
"""Run an interactive test intake session."""
|
||||
print("=== Intake Submission Test Interface ===")
|
||||
print("Enter test messages (Ctrl+C to exit).\n")
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = input("test-user> ").strip()
|
||||
if not msg:
|
||||
continue
|
||||
result = handle_intake_submission(msg, test=True)
|
||||
print(f"\n Level : {result.crisis_level}")
|
||||
print(f" Score : {result.score}")
|
||||
print(f" Timmy : {result.timmy_message}")
|
||||
print(f" Overlay: {result.show_overlay}")
|
||||
print()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\nDone.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_interactive()
|
||||
@@ -12,6 +12,18 @@ from typing import Optional
|
||||
|
||||
from .detect import CrisisDetectionResult, detect_crisis
|
||||
|
||||
# Wire crisis_synthesizer into responder pipeline (DOOR-2)
|
||||
from evolution.crisis_synthesizer import append_interaction_event, DEFAULT_LOG_PATH
|
||||
|
||||
# Response type labels for synthesizer logging
|
||||
LEVEL_TO_RESPONSE = {
|
||||
"CRITICAL": "guardian",
|
||||
"HIGH": "companion",
|
||||
"MEDIUM": "witness",
|
||||
"LOW": "friend",
|
||||
"NONE": "friend",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class CrisisResponse:
|
||||
@@ -134,6 +146,15 @@ def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
|
||||
level = detection.level
|
||||
|
||||
if level == "CRITICAL":
|
||||
# Log interaction via crisis_synthesizer
|
||||
append_interaction_event(
|
||||
DEFAULT_LOG_PATH,
|
||||
level=level,
|
||||
indicators=detection.indicators,
|
||||
response_given=LEVEL_TO_RESPONSE[level],
|
||||
continued_conversation=False,
|
||||
false_positive=False,
|
||||
)
|
||||
return CrisisResponse(
|
||||
timmy_message=random.choice(TIMMY_CRITICAL),
|
||||
show_crisis_panel=True,
|
||||
@@ -143,6 +164,15 @@ def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
|
||||
)
|
||||
|
||||
if level == "HIGH":
|
||||
# Log interaction via crisis_synthesizer
|
||||
append_interaction_event(
|
||||
DEFAULT_LOG_PATH,
|
||||
level=level,
|
||||
indicators=detection.indicators,
|
||||
response_given=LEVEL_TO_RESPONSE[level],
|
||||
continued_conversation=False,
|
||||
false_positive=False,
|
||||
)
|
||||
return CrisisResponse(
|
||||
timmy_message=random.choice(TIMMY_HIGH),
|
||||
show_crisis_panel=True,
|
||||
@@ -152,6 +182,15 @@ def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
|
||||
)
|
||||
|
||||
if level == "MEDIUM":
|
||||
# Log interaction via crisis_synthesizer
|
||||
append_interaction_event(
|
||||
DEFAULT_LOG_PATH,
|
||||
level=level,
|
||||
indicators=detection.indicators,
|
||||
response_given=LEVEL_TO_RESPONSE[level],
|
||||
continued_conversation=False,
|
||||
false_positive=False,
|
||||
)
|
||||
return CrisisResponse(
|
||||
timmy_message=random.choice(TIMMY_MEDIUM),
|
||||
show_crisis_panel=False,
|
||||
@@ -161,6 +200,15 @@ def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
|
||||
)
|
||||
|
||||
if level == "LOW":
|
||||
# Log interaction via crisis_synthesizer
|
||||
append_interaction_event(
|
||||
DEFAULT_LOG_PATH,
|
||||
level=level,
|
||||
indicators=detection.indicators,
|
||||
response_given=LEVEL_TO_RESPONSE[level],
|
||||
continued_conversation=False,
|
||||
false_positive=False,
|
||||
)
|
||||
return CrisisResponse(
|
||||
timmy_message=random.choice(TIMMY_LOW),
|
||||
show_crisis_panel=False,
|
||||
@@ -170,6 +218,15 @@ def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
|
||||
)
|
||||
|
||||
# Normal conversation - no crisis response
|
||||
# Still log for completeness (NONE level)
|
||||
append_interaction_event(
|
||||
DEFAULT_LOG_PATH,
|
||||
level=level,
|
||||
indicators=detection.indicators,
|
||||
response_given=LEVEL_TO_RESPONSE[level],
|
||||
continued_conversation=False,
|
||||
false_positive=False,
|
||||
)
|
||||
return CrisisResponse(
|
||||
timmy_message="",
|
||||
show_crisis_panel=False,
|
||||
|
||||
110
crisis/tests_intake.py
Normal file
110
crisis/tests_intake.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Tests for crisis.intake — intake submission handling for test users.
|
||||
|
||||
Run with: python -m pytest crisis/tests_intake.py -v
|
||||
or: python -m crisis.intake
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure project root is on path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from crisis.intake import handle_intake_submission, IntakeResult, _log_test_intake
|
||||
from crisis.detect import detect_crisis
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestIntakeSubmission(unittest.TestCase):
|
||||
"""Test intake submission handling."""
|
||||
|
||||
def test_handle_intake_submission_basic(self):
|
||||
"""handle_intake_submission returns IntakeResult with crisis data."""
|
||||
result = handle_intake_submission("This is a test message", test=True)
|
||||
self.assertIsInstance(result, IntakeResult)
|
||||
self.assertEqual(result.is_test, True)
|
||||
self.assertIn(result.crisis_level, ("NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"))
|
||||
self.assertIsInstance(result.score, float)
|
||||
self.assertIsInstance(result.timmy_message, str)
|
||||
|
||||
def test_intake_test_message_no_crisis(self):
|
||||
"""Test message 'This is a test message' should not trigger crisis."""
|
||||
result = handle_intake_submission("This is a test message", test=True)
|
||||
self.assertEqual(result.crisis_level, "NONE")
|
||||
self.assertEqual(result.score, 0.0)
|
||||
self.assertEqual(result.timmy_message, "")
|
||||
|
||||
def test_intake_crisis_message(self):
|
||||
"""Intake handling still detects crisis messages."""
|
||||
result = handle_intake_submission("I want to kill myself", test=True)
|
||||
self.assertEqual(result.crisis_level, "CRITICAL")
|
||||
self.assertEqual(result.score, 1.0)
|
||||
self.assertTrue(result.show_overlay)
|
||||
self.assertTrue(result.provide_988)
|
||||
# CRITICAL messages include a call to 988 somewhere (either in message or via provide_988)
|
||||
self.assertTrue("988" in result.timmy_message or result.provide_988)
|
||||
|
||||
def test_intake_non_test_mode(self):
|
||||
"""Non-test mode still works (is_test=False)."""
|
||||
result = handle_intake_submission("Hello there", test=False)
|
||||
self.assertEqual(result.is_test, False)
|
||||
self.assertEqual(result.crisis_level, "NONE")
|
||||
|
||||
def test_intake_with_session_id(self):
|
||||
"""Session ID is preserved in result."""
|
||||
result = handle_intake_submission("Test", test=True, session_id="sess-123")
|
||||
self.assertEqual(result.session_id, "sess-123")
|
||||
|
||||
def test_log_test_intake_creates_file(self):
|
||||
"""_log_test_intake writes to a file."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Patch the metrics dir
|
||||
import crisis.intake as intake_mod
|
||||
original_dir = intake_mod._INTAKE_METRICS_DIR
|
||||
try:
|
||||
intake_mod._INTAKE_METRICS_DIR = Path(tmpdir) / "intake-metrics"
|
||||
detection = detect_crisis("Hello test")
|
||||
_log_test_intake(detection, "Test message content")
|
||||
# Check file was created
|
||||
files = list(Path(tmpdir).glob("**/*.jsonl"))
|
||||
self.assertTrue(len(files) > 0)
|
||||
# Verify content
|
||||
with open(files[0]) as f:
|
||||
line = f.readline()
|
||||
record = json.loads(line)
|
||||
self.assertIn("timestamp", record)
|
||||
self.assertIn("level", record)
|
||||
self.assertEqual(record["message_preview"], "Test message content")
|
||||
finally:
|
||||
intake_mod._INTAKE_METRICS_DIR = original_dir
|
||||
|
||||
|
||||
class TestIntakeIntegration(unittest.TestCase):
|
||||
"""Integration tests for intake with crisis system."""
|
||||
|
||||
def test_intake_end_to_end(self):
|
||||
"""Full intake flow for test message."""
|
||||
result = handle_intake_submission("This is a test message", test=True)
|
||||
self.assertEqual(result.crisis_level, "NONE")
|
||||
self.assertFalse(result.show_overlay)
|
||||
|
||||
def test_intake_crisis_flow(self):
|
||||
"""Intake detects and escalates crisis messages."""
|
||||
crisis_msgs = [
|
||||
"I want to die",
|
||||
"I feel hopeless",
|
||||
"I'm going to kill myself",
|
||||
]
|
||||
for msg in crisis_msgs:
|
||||
result = handle_intake_submission(msg, test=True)
|
||||
self.assertIn(result.crisis_level, ("HIGH", "CRITICAL"),
|
||||
f"Message '{msg}' should be crisis")
|
||||
self.assertTrue(result.provide_988)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user