import { Hono } from "hono"; import { streamSSE } from "hono/streaming"; import { z } from "zod"; import { randomUUID } from "node:crypto"; import { SessionStore } from "./session.js"; import { encodeSseFrame, retrieve, tokenizeAnswer } from "./stream.js"; const QuerySchema = z.object({ sessionId: z.string().min(1).optional(), role: z.string().min(1).optional(), jurisdiction: z.string().min(1).optional(), q: z.string().min(1), }); export type AppOptions = { sessionStore?: SessionStore; tokenDelayMs?: number; }; function renderClient(): string { return ` Production RAG chatbot

Capstone 08 chat (skeleton)

Role: analyst, jurisdiction: GDPR. Streams SSE token-by-token.

`; } export function buildApp(options: AppOptions = {}): { app: Hono; sessions: SessionStore; } { const sessions = options.sessionStore ?? new SessionStore(); const tokenDelayMs = options.tokenDelayMs ?? 0; const app = new Hono(); app.get("/", (c) => c.html(renderClient())); app.get("/health", (c) => c.json({ ok: true, sessions: sessions.size() })); app.get("/sessions", (c) => { const list = sessions.list().map((s) => ({ id: s.id, role: s.role, jurisdiction: s.jurisdiction, turnCount: s.turns.length, })); return c.json({ sessions: list }); }); app.get("/chat/stream", (c) => { const parsed = QuerySchema.safeParse({ sessionId: c.req.query("sessionId"), role: c.req.query("role"), jurisdiction: c.req.query("jurisdiction"), q: c.req.query("q"), }); if (!parsed.success) { return c.json({ error: "missing q" }, 400); } const sessionId = parsed.data.sessionId ?? randomUUID(); const role = parsed.data.role ?? "analyst"; const jurisdiction = parsed.data.jurisdiction ?? "GDPR"; const q = parsed.data.q; const session = sessions.getOrCreate(sessionId, role, jurisdiction); sessions.appendTurn(sessionId, { role: "user", content: q, ts: Date.now() }); return streamSSE(c, async (stream) => { const writeFrame = async (event: string, data: unknown): Promise => { await stream.write(encodeSseFrame(event, data)); }; await writeFrame("session", { sessionId, role, jurisdiction, turn: session.turns.length, }); const citations = retrieve(q, jurisdiction, 3); await writeFrame("citations", { items: citations }); const tokens = tokenizeAnswer(q, citations); let assembled = ""; for (const tok of tokens) { if (stream.aborted) return; assembled += tok; await writeFrame("token", { text: tok }); if (tokenDelayMs > 0) await stream.sleep(tokenDelayMs); } sessions.appendTurn(sessionId, { role: "assistant", content: assembled, ts: Date.now(), }); await writeFrame("done", { totalTokens: tokens.length }); }); }); app.notFound((c) => c.json({ error: "not found" }, 404)); return { app, sessions }; }