| import Fastify from "fastify"; | |
| import cors from "@fastify/cors"; | |
| import { z } from "zod"; | |
| import { handleChat } from "./ai/engine.js"; | |
| import { ChatRequest } from "./types.js"; | |
| const server = Fastify({ | |
| logger: true | |
| }); | |
| await server.register(cors, { | |
| origin: true, | |
| credentials: true | |
| }); | |
| server.get("/health", async () => ({ status: "ok" })); | |
| server.post("/api/chat", async (request, reply) => { | |
| const body = request.body as ChatRequest; | |
| const schema = z.object({ | |
| sessionId: z.string().min(1), | |
| message: z.string().min(1), | |
| mode: z.enum(["auto", "search", "no-search"]).optional() | |
| }); | |
| const parsed = schema.safeParse(body); | |
| if (!parsed.success) { | |
| reply.status(400); | |
| return { error: parsed.error.flatten() }; | |
| } | |
| const chunks: string[] = []; | |
| await handleChat(parsed.data, { | |
| onStatus: () => {}, | |
| onToken: (token) => chunks.push(token), | |
| onDone: () => {}, | |
| onError: () => {} | |
| }); | |
| return { message: chunks.join("") }; | |
| }); | |
| server.get("/api/chat/stream", async (request, reply) => { | |
| const query = request.query as Record<string, string>; | |
| const schema = z.object({ | |
| sessionId: z.string().min(1), | |
| message: z.string().min(1), | |
| mode: z.enum(["auto", "search", "no-search"]).optional() | |
| }); | |
| const parsed = schema.safeParse(query); | |
| if (!parsed.success) { | |
| reply.status(400); | |
| return reply.send({ error: parsed.error.flatten() }); | |
| } | |
| reply.raw.writeHead(200, { | |
| "Content-Type": "text/event-stream", | |
| "Cache-Control": "no-cache, no-transform", | |
| Connection: "keep-alive", | |
| "X-Accel-Buffering": "no" | |
| }); | |
| reply.raw.flushHeaders?.(); | |
| const writeEvent = (event: string, data: string) => { | |
| reply.raw.write(`event: ${event}\n`); | |
| reply.raw.write(`data: ${data}\n\n`); | |
| }; | |
| const requestPayload: ChatRequest = { | |
| sessionId: parsed.data.sessionId, | |
| message: parsed.data.message, | |
| mode: parsed.data.mode | |
| }; | |
| let closed = false; | |
| request.raw.on("close", () => { | |
| closed = true; | |
| }); | |
| await handleChat(requestPayload, { | |
| onStatus: (message) => !closed && writeEvent("status", JSON.stringify({ message })), | |
| onToken: (token) => !closed && writeEvent("delta", JSON.stringify({ token })), | |
| onDone: () => !closed && writeEvent("done", JSON.stringify({ ok: true })), | |
| onError: (message) => !closed && writeEvent("error", JSON.stringify({ message })) | |
| }); | |
| reply.raw.end(); | |
| }); | |
| const port = Number(process.env.PORT ?? 8080); | |
| server.listen({ port, host: "0.0.0.0" }).catch((err) => { | |
| server.log.error(err); | |
| process.exit(1); | |
| }); | |