62 lines
2.5 KiB
TypeScript

// Parser für Lyras LLM-JSON-Antworten + Emotion-Detection.
import type { Emotion as LyraEmotion } from '../components/RiveAvatar';
import { EMPATHY_RE, HAPPY_RE } from './sosConstants';
export type { LyraEmotion };
export type ChipSpec = { label: string; action: string };
// Parse LLM JSON response — robust gegen Markdown-Fences UND abgeschnittenes JSON
export function parseLyraResponse(raw: string): { message: string; chips: ChipSpec[] } {
if (!raw) return { message: '', chips: [] };
// Strip ALL markdown fences (auch wenn nur am Anfang)
const text = raw.trim()
.replace(/^```(?:json)?\s*/i, '')
.replace(/```\s*$/i, '')
.trim();
const start = text.indexOf('{');
if (start === -1) return { message: raw.trim(), chips: [] };
// Erst echtes JSON-Parse versuchen (komplett)
const end = text.lastIndexOf('}');
if (end > start) {
try {
const obj = JSON.parse(text.slice(start, end + 1));
const message = typeof obj.message === 'string' ? obj.message.trim() : '';
const chipsRaw = Array.isArray(obj.chips) ? obj.chips : [];
const chips: ChipSpec[] = chipsRaw
.filter((c: { label?: unknown; action?: unknown }) => c && typeof c.label === 'string' && typeof c.action === 'string')
.slice(0, 5)
.map((c: { label: string; action: string }) => ({ label: c.label.trim(), action: c.action.trim() }));
if (message) return { message, chips };
} catch {/* fall through to recovery */}
}
// RECOVERY: JSON ist abgeschnitten (z.B. max_tokens hit).
// 1) message-Feld per Regex extrahieren
const msgMatch = text.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"/);
let message = '';
if (msgMatch) {
try { message = JSON.parse('"' + msgMatch[1] + '"'); } catch { message = msgMatch[1]; }
}
// 2) Chips: alle vollständigen {label,action}-Objekte einsammeln
const chips: ChipSpec[] = [];
const chipRe = /\{\s*"label"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"action"\s*:\s*"((?:[^"\\]|\\.)*)"\s*\}/g;
let m: RegExpExecArray | null;
while ((m = chipRe.exec(text)) !== null && chips.length < 5) {
try {
chips.push({
label: JSON.parse('"' + m[1] + '"').trim(),
action: JSON.parse('"' + m[2] + '"').trim(),
});
} catch {
chips.push({ label: m[1].trim(), action: m[2].trim() });
}
}
if (message) return { message, chips };
return { message: raw.trim(), chips: [] };
}
export function detectEmotion(text: string): LyraEmotion {
if (HAPPY_RE.test(text)) return 'happy';
if (EMPATHY_RE.test(text)) return 'empathy';
return 'idle';
}