/** * POST /api/lyra/memories/extract * * Extrahiert strukturierte Memories aus einem SOS/Coach-Gespräch via LLM (Claude Haiku). * Wird intern (fire-and-forget) nach SOS-Stream-Ende aufgerufen. * * Body: { sessionId: string, conversation: Array<{role, content}> } * Response: { extracted: number, skipped: number } * * Fehler: silent — User darf nichts merken. Logging via [lyra-memory]. */ import { upsertMemory } from "../../../db/lyraMemory"; import type { LyraMemoryType } from "../../../db/lyraMemory"; const VALID_TYPES: LyraMemoryType[] = [ "trigger", "habit", "strength", "relationship", "milestone", "pain_point", "goal", "preference", ]; const EXTRACTION_SYSTEM_PROMPT = `Du extrahierst aus einem Gespräch zwischen User und Lyra-Coach strukturierte Fakten über den User. Output strikt als JSON-Array: [{"type":"trigger|habit|strength|relationship|milestone|pain_point|goal|preference", "content":"", "confidence":0.0-1.0}] Regeln: - Nur Fakten die der USER explizit oder implizit über sich geteilt hat. Nichts erfinden. - Keine Vermutungen über Diagnosen oder Pathologisierungen ("süchtig", "krank" etc). - Nur Wesentliches. Wenn nichts Neues drin ist → leeres Array []. - confidence: 0.9+ wenn User explizit gesagt, 0.5-0.7 wenn implizit, <0.5 garnicht extrahieren. - content in der Sprache des Gesprächs (DE). - Maximal 8 Einträge pro Extraktion.`; export default defineEventHandler(async (event) => { const user = await requireUser(event); const body = await readBody(event); const { sessionId, conversation, }: { sessionId?: string; conversation: Array<{ role: string; content: string }>; } = body; if (!Array.isArray(conversation) || conversation.length === 0) { throw createError({ statusCode: 400, message: "conversation fehlt" }); } const config = useRuntimeConfig(); const key = config.openrouterApiKey as string | undefined; if (!key) { throw createError({ statusCode: 503, message: "OpenRouter Key fehlt" }); } // Nur User-Nachrichten extrahieren (Lyra-Antworten sind Kontext, kein User-Fakt) const userMessages = conversation.filter((m) => m.role === "user"); if (userMessages.length === 0) { return { extracted: 0, skipped: 0 }; } // Konversation als lesbaren Text aufbereiten (max 4000 chars für Haiku) const conversationText = conversation .slice(-20) // letzte 20 Messages reichen für Kontext .map((m) => `${m.role === "user" ? "User" : "Lyra"}: ${m.content}`) .join("\n") .slice(0, 4000); let extracted = 0; let skipped = 0; try { const res = await $fetch<{ choices: { message: { content: string } }[]; }>("https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", "HTTP-Referer": "https://rebreak.org", "X-Title": "ReBreak Memory Extraction", }, body: { model: "anthropic/claude-haiku-4-5", max_tokens: 800, temperature: 0.1, messages: [ { role: "system", content: EXTRACTION_SYSTEM_PROMPT }, { role: "user", content: conversationText }, ], }, timeout: 20000, }); const raw = res.choices?.[0]?.message?.content?.trim(); if (!raw) { console.log("[lyra-memory] extract: empty response from LLM"); return { extracted: 0, skipped: 0 }; } // JSON-Array parsen (Haiku gibt manchmal Markdown-Fences zurück) const jsonStr = raw .replace(/^```(?:json)?\s*/i, "") .replace(/\s*```$/, "") .trim(); let facts: Array<{ type: string; content: string; confidence: number; }> = []; try { facts = JSON.parse(jsonStr); } catch { console.warn( "[lyra-memory] extract: JSON parse failed:", jsonStr.slice(0, 200), ); return { extracted: 0, skipped: 0 }; } if (!Array.isArray(facts)) { return { extracted: 0, skipped: 0 }; } for (const fact of facts) { // Confidence < 0.5 → skip if (!fact.content || (fact.confidence ?? 0) < 0.5) { skipped++; continue; } // Typ validieren if (!VALID_TYPES.includes(fact.type as LyraMemoryType)) { console.warn("[lyra-memory] extract: invalid type:", fact.type); skipped++; continue; } try { await upsertMemory( user.id, fact.type as LyraMemoryType, fact.content, sessionId ?? "sos-session", Math.min(1, Math.max(0, fact.confidence ?? 0.7)), ); extracted++; } catch (e) { console.error("[lyra-memory] upsert error:", e); skipped++; } } console.log( `[lyra-memory] extract done for ${user.id}: ${extracted} extracted, ${skipped} skipped`, ); } catch (e) { // Silent fail — User darf nichts merken console.error("[lyra-memory] extract LLM error:", e); return { extracted: 0, skipped: 0 }; } return { success: true, data: { extracted, skipped } }; });