LLM-unabhaengiges Sicherheitsnetz fuer Lyras SOS-Pfad, schliesst das Top-Risiko der Risiko-Akte (verpasste Krise, ISO 14971 R-LYRA-01). Backend: - crisis-filter.ts: deterministische Krisen-/Suizid-Erkennung (DE primaer, EN/FR/AR Grundabdeckung) auf den letzten User-Nachrichten, synchron, kein LLM - sos-session.post: liefert crisisLevel sofort an die App (vor Stream-Start) - sos-stream: sendet bei Krise zuerst 'crisis_chips' (BZgA/112/Telefonseelsorge); Fallback an 3 Stellen (LLM-Fehler/Abbruch/keine Chips) -> nie leerer Screen - 43/43 Unit-Tests (crisis.json positiv, harmless.json False-Positive-Guard) Frontend (urge.tsx): - permanente rote Krisen-Bar oben, durch LLM-Chips nicht ueberschreibbar (eigener State-Slot), Hotline-Chips als tel:-Links - neue Locale-Strings DE/EN Risiko-Akte: R-LYRA-01 Restrisiko HOCH -> MITTEL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
/**
|
|
* POST /api/coach/sos-session — Erstellt Session für SSE-Stream
|
|
*
|
|
* Client sendet messages + locale, Backend generiert sessionId
|
|
* und speichert Daten in-memory. Client nutzt dann GET /api/coach/sos-stream?session=xyz
|
|
*
|
|
* Grund: react-native-sse (EventSource API) unterstützt nur GET, nicht POST.
|
|
* Daher 2-Step-Flow: POST Session erstellen → GET Stream öffnen.
|
|
*
|
|
* Safety: Deterministischer Crisis-Pre-Filter (crisis-filter.ts) läuft HIER,
|
|
* bevor das LLM überhaupt gestartet wird. Das Ergebnis (crisisLevel) wird in
|
|
* der Session gespeichert und von sos-stream.get.ts genutzt um Krisen-Chips
|
|
* LLM-unabhängig einzublenden.
|
|
*/
|
|
import { detectCrisis } from "../../utils/crisis-filter";
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
const body = await readBody(event);
|
|
const { messages, locale, llmProvider } = body as {
|
|
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
locale?: string;
|
|
llmProvider?: string;
|
|
};
|
|
|
|
if (!messages || !Array.isArray(messages)) {
|
|
throw createError({ statusCode: 400, message: "messages fehlt" });
|
|
}
|
|
|
|
// ── Deterministischer Crisis-Pre-Filter ──────────────────────────────────
|
|
// Prüfe die letzten 3 User-Nachrichten (nicht nur die letzte — Kontext zählt).
|
|
// Läuft synchron, kein Overhead, kein LLM-Aufruf.
|
|
const userTexts = messages
|
|
.filter((m) => m.role === "user")
|
|
.slice(-3)
|
|
.map((m) => m.content);
|
|
|
|
const crisisResult = detectCrisis(userTexts);
|
|
|
|
if (crisisResult.isCrisis) {
|
|
console.log(
|
|
`[crisis-filter] MATCH user=${user.id} level=${crisisResult.level} ` +
|
|
`group=${crisisResult.matchedGroup} pattern="${crisisResult.matchedPattern}"`,
|
|
);
|
|
}
|
|
|
|
// Session-ID generieren
|
|
const sessionId = `sos_${user.id}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
|
|
// In globalem Store speichern (siehe server/utils/sosSessions.ts)
|
|
const { setSosSession } = await import("../../utils/sosSessions");
|
|
setSosSession(sessionId, {
|
|
userId: user.id,
|
|
messages,
|
|
locale: locale ?? "de",
|
|
llmProvider,
|
|
createdAt: Date.now(),
|
|
crisisLevel: crisisResult.level,
|
|
});
|
|
|
|
return {
|
|
sessionId,
|
|
// crisisDetected wird transparent ans Frontend zurückgegeben —
|
|
// Frontend kann damit sofort (noch vor Stream-Start) die Krisen-UI aktivieren.
|
|
crisisDetected: crisisResult.isCrisis,
|
|
crisisLevel: crisisResult.level,
|
|
};
|
|
});
|