import { streamChat } from "@/lib/vllm"; import { sendMessage } from "@/lib/synap"; import { assembleBrief } from "@/lib/briefing"; import { addPersonalizationHints, ensurePersonalizationSession, ensurePersonalizationUser, recordObserverPersonalization, } from "@/lib/tidal-personalization"; import type { ObserverOutput } from "@/lib/types"; interface ChatBody { messages: { role: "user" | "assistant"; content: string }[]; conversationId?: string; personId?: string; } // --- Per-conversation state (in-memory, lost on restart — fine for M3) --- const turnCounts = new Map(); const signalBuffers = new Map(); const lastTopics = new Map(); const SYNTHESIS_INTERVAL = 5; export async function POST(req: Request) { const requestTimestamp = Date.now(); let body: ChatBody; try { body = await req.json(); } catch { return new Response("Invalid JSON", { status: 400 }); } if (!body.messages?.length) { return new Response("No messages provided", { status: 400 }); } const lastUserMsg = body.messages.findLast((m) => m.role === "user"); const conversationId = body.conversationId; const personId = body.personId; // Keep tidal personalization state hot, but never block chat if unavailable. if (personId) { ensurePersonalizationUser(personId).catch((err) => console.error("[tidal] ensure user failed:", err) ); } if (personId && conversationId) { ensurePersonalizationSession(conversationId, personId).catch((err) => console.error("[tidal] ensure session failed:", err) ); } // 1. Store user message in Synap (non-blocking — don't delay stream start) if (conversationId && lastUserMsg) { sendMessage("user", lastUserMsg.content, conversationId).catch((err) => console.error("[synap] failed to store user message:", err.message) ); } // 2. Assemble communication brief (replaces scatter-shot recall + cohort loading) const synapBrief = personId ? await assembleBrief(personId).catch((err) => { console.error("[brief] assembly failed:", err.message); return undefined; }) : undefined; const brief = personId ? await addPersonalizationHints(personId, synapBrief) : synapBrief; const encoder = new TextEncoder(); let fullResponse = ""; const stream = new ReadableStream({ async start(controller) { try { for await (const token of streamChat(body.messages, brief)) { fullResponse += token; controller.enqueue( encoder.encode(`data: ${JSON.stringify({ token })}\n\n`) ); } controller.enqueue(encoder.encode("data: [DONE]\n\n")); } catch (err) { const message = err instanceof Error ? err.message : "Connection failed"; controller.enqueue( encoder.encode(`data: ${JSON.stringify({ error: message })}\n\n`) ); } finally { controller.close(); // 5. Store assistant response in Synap (after stream ends) if (conversationId && fullResponse) { sendMessage("aeries", fullResponse, conversationId).catch((err) => console.error( "[synap] failed to store assistant message:", err.message ) ); } // 6. Fire deep observer (non-blocking) if (conversationId && lastUserMsg && fullResponse) { fireDeepObserver( lastUserMsg.content, fullResponse, conversationId, requestTimestamp, body.messages, personId ).catch((err) => console.error("[observe] failed:", err.message) ); } } }, }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, }); } /** Deep observer: Tier 1 structured extraction + Tier 2 periodic synthesis + profile update. */ async function fireDeepObserver( userMessage: string, assistantMessage: string, conversationId: string, requestTimestamp: number, allMessages: { role: string; content: string }[], personId?: string ): Promise { const { extractObserverOutput, outputToSignalMemories, storeSignals, synthesizeObservations, storeObservations, } = await import("@/lib/observer"); // Track turns const turn = (turnCounts.get(conversationId) ?? 0) + 1; turnCounts.set(conversationId, turn); // Compute latency (time between request arrival and now — approximation) const latencySeconds = Math.round((Date.now() - requestTimestamp) / 1000); // Tier 1: Structured extraction const output = await extractObserverOutput(userMessage, assistantMessage, { turnNumber: turn, latencySeconds, previousTopic: lastTopics.get(conversationId), }); if (!output) { console.log(`[observer] turn ${turn}: extraction returned null`); return; } console.log( `[observer] turn ${turn}: topic=${output.topic.primary}, sentiment=${output.engagement.sentiment_score}, formality=${output.style.formality}` ); // Track topic for next turn's context lastTopics.set(conversationId, output.topic.primary); // Store dimension-tagged signals in Synap (with person tag) const memories = outputToSignalMemories(output, conversationId, personId); await storeSignals(memories); // Buffer for synthesis const buffer = signalBuffers.get(conversationId) ?? []; buffer.push(output); // Keep only last 10 to bound memory if (buffer.length > 10) buffer.shift(); signalBuffers.set(conversationId, buffer); // Tier 2: Periodic synthesis (every N turns) if (turn % SYNTHESIS_INTERVAL === 0 && buffer.length >= 3) { console.log(`[observer] turn ${turn}: running Tier 2 synthesis`); // Build conversation snippet from last few messages const snippet = allMessages .slice(-6) .map((m) => `${m.role}: ${m.content.slice(0, 100)}`) .join("\n"); const observations = await synthesizeObservations(buffer, snippet); if (observations.length) { await storeObservations(observations, conversationId, personId); } } if (personId) { await recordObserverPersonalization({ personId, conversationId, turn, assistantMessage, output, }).catch((err) => console.error("[tidal] observer personalization write failed:", err) ); } // M4: Update person profile after signal extraction if (personId) { const { computeProfile, storeProfile, loadProfile } = await import( "@/lib/cohorts" ); const existing = await loadProfile(personId); const updated = computeProfile([output], existing, personId); console.log( `[cohorts] updated profile for ${personId.slice(0, 8)}…: interactions=${updated.interactionCount}, cohorts=[${updated.cohorts.map((c) => `${c.cohort}(${c.probability.toFixed(2)})`).join(", ")}]` ); await storeProfile(updated); } }