import { NextResponse } from "next/server"; import { cookies } from "next/headers"; import { sql } from "@/db"; import { detectMedicalIntent, ESCALATION_RULES } from "@/lib/ai/medical-triggers"; import { classifyIntent } from "@/lib/ai/classifier"; import { answerStructuredQuery } from "@/lib/ai/structured-query"; import { answerMemoryQuery } from "@/lib/ai/memory-search"; import { answerParentingQuery } from "@/lib/ai/parenting"; import { logAudit } from "@/lib/audit"; import { requireFamily } from "@/lib/auth"; export async function POST(request: Request) { const start = Date.now(); try { const auth = await requireFamily(); if (!auth.success) { return NextResponse.json({ error: auth.error }, { status: auth.status }); } const session = auth.session!; const familyId = session.familyId!; const body = await request.json(); const { messages, childId } = body as { messages: { role: "user" | "assistant"; content: string }[]; childId?: string; }; if (!messages || !Array.isArray(messages)) { return NextResponse.json({ error: "messages array required" }, { status: 400 }); } const lastUserMsg = [...messages].reverse().find(m => m.role === "user")?.content || ""; if (!lastUserMsg.trim()) { return NextResponse.json({ error: "Empty message" }, { status: 400 }); } // ── 1. HARD GUARDRAIL: keyword-based medical detection (most conservative) ── const medicalIntent = detectMedicalIntent(lastUserMsg); if (medicalIntent.isMedical) { const families = await sql`SELECT pediatrician_phone FROM families WHERE id = ${familyId} LIMIT 1`; const phone = families[0]?.pediatrician_phone; const reply = [ `I can't interpret symptoms — that's a pediatrician's job, not mine.`, ``, ESCALATION_RULES[medicalIntent.category], ``, phone ? `Call your pediatrician now: ${phone}` : `Add your pediatrician's phone in Settings so I can show it here.`, ].join("\n"); await logAudit({ action: "ai_medical_redirect", metadata: { category: medicalIntent.category, keyword: medicalIntent.matchedKeyword }, request, userId: session.userId, familyId, }); await logUsage({ familyId, userId: session.userId, intent: "medical_redirect", durationMs: Date.now() - start }); return NextResponse.json({ reply, redirected: true, category: medicalIntent.category }); } // ── 2. Resolve child context ────────────────────────────────────────────── let childName = "your baby"; let childAge = "unknown age"; let birthDate = ""; const resolvedChildId = childId || null; if (resolvedChildId) { const kids = await sql`SELECT name, birth_date FROM children WHERE id = ${resolvedChildId} AND family_id = ${familyId} LIMIT 1`; if (kids[0]) { childName = kids[0].name; birthDate = kids[0].birth_date; childAge = calculateAge(birthDate); } } // ── 3. LLM-based intent classification ─────────────────────────────────── const classification = await classifyIntent(lastUserMsg); // Medical_redirect from classifier → also redirect if (classification.intent === "medical_redirect") { const families = await sql`SELECT pediatrician_phone FROM families WHERE id = ${familyId} LIMIT 1`; const phone = families[0]?.pediatrician_phone; const reply = [ `That sounds like something your pediatrician should assess.`, ``, ESCALATION_RULES["default"], phone ? `Call: ${phone}` : `Add your pediatrician's phone in Settings.`, ].join("\n"); await logUsage({ familyId, userId: session.userId, intent: "medical_redirect", durationMs: Date.now() - start }); return NextResponse.json({ reply, redirected: true }); } // ── 4. Route to appropriate handler ────────────────────────────────────── let reply: string; let memories: unknown[] | undefined; if (classification.intent === "structured_query" && resolvedChildId) { reply = await answerStructuredQuery(lastUserMsg, { childId: resolvedChildId, familyId, childName, birthDate, }); } else if (classification.intent === "memory_search") { const cookieStore = await cookies(); const sessionCookie = `tia_session=${cookieStore.get("tia_session")?.value || ""}`; const result = await answerMemoryQuery(lastUserMsg, resolvedChildId, sessionCookie); reply = result.text; memories = result.memories; } else { // general_parenting (default) const history = messages.slice(0, -1); // all but the last user message reply = await answerParentingQuery(lastUserMsg, history, { childName, childAge }); } await logUsage({ familyId, userId: session.userId, intent: classification.intent, durationMs: Date.now() - start, }); await logAudit({ action: "ai_query", metadata: { intent: classification.intent, confidence: classification.confidence }, request, userId: session.userId, familyId, }); return NextResponse.json({ reply, memories, intent: classification.intent }); } catch (error) { console.error("[ai/route]", error); return NextResponse.json({ error: String(error) }, { status: 500 }); } } // ── Helpers ────────────────────────────────────────────────────────────────── function calculateAge(birthDate: string): string { if (!birthDate) return "unknown age"; const birth = new Date(birthDate); const now = new Date(); const totalDays = Math.floor((now.getTime() - birth.getTime()) / 86400000); const months = Math.floor(totalDays / 30.44); const years = Math.floor(months / 12); if (years > 0) return `${years} year${years > 1 ? "s" : ""} ${months % 12} month${months % 12 !== 1 ? "s" : ""}`; if (months > 0) return `${months} month${months > 1 ? "s" : ""}`; return `${totalDays} day${totalDays !== 1 ? "s" : ""}`; } async function logUsage(opts: { familyId: string; userId?: string; intent: string; durationMs: number; }) { try { await sql` INSERT INTO ai_usage (family_id, user_id, intent, duration_ms) VALUES (${opts.familyId}, ${opts.userId || null}, ${opts.intent}, ${opts.durationMs}) `; } catch { // Non-critical — don't break the response } }