export declare const ragRoutesTemplate = "import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { getLLMConfig, createChatModel } from \"@/services/llm\";\nimport {\n searchDocs,\n ensureDocsIndex,\n getIndexStatus,\n} from \"@/services/mcp/server\";\nimport { rateLimit } from \"@/utils/rateLimit\";\nimport { config } from \"@/utils/config\";\nimport { isSiteRequestAuthorized } from \"@/lib/access\";\nimport { timingSafeEqual } from \"@/lib/siteGate\";\nimport { readJsonBody, RequestTooLargeError } from \"@/utils/requestBody\";\n\n// Covers the schema's worst-case UTF-8 payload while retaining a hard cap.\nconst MAX_RAG_REQUEST_BYTES = 512 * 1024;\n\nconst messageSchema = z\n .object({\n role: z.enum([\"user\", \"assistant\"]),\n content: z.string().max(4000),\n })\n .strict();\n\nconst ragSchema = z\n .object({\n question: z.string().min(1).max(2000),\n history: z.array(messageSchema).max(20).optional(),\n })\n .strict();\n\nfunction bearerToken(req: Request): string | null {\n const authorization = req.headers.get(\"authorization\");\n return authorization?.startsWith(\"Bearer \")\n ? authorization.slice(\"Bearer \".length)\n : null;\n}\n\nasync function isRagRequestAuthorized(req: Request): Promise {\n const siteAuthorized = await isSiteRequestAuthorized();\n if (process.env.SITE_PASSWORD && siteAuthorized) return true;\n\n const apiKey = process.env.RAG_API_KEY;\n if (apiKey) {\n const token = bearerToken(req);\n return token !== null && timingSafeEqual(token, apiKey);\n }\n return siteAuthorized;\n}\n\nconst projectName = config.name || \"Doccupine\";\n\nconst systemContext = `You are AI Assistant, a documentation assistant for ${projectName}, Your name is ${projectName} AI Assistant.\n\n## Core Rules\n1. Answer ONLY from the provided context. Never fabricate information.\n2. If the answer isn't in the context, say so clearly and suggest relevant sections or pages the user might check.\n3. If the question is ambiguous, ask a brief clarifying question before answering.\n\n## Response Style\n- Be concise and direct. Lead with the answer, then provide details if needed.\n- Use code examples from the context when relevant.\n- Match the technical level of the user's question.\n\n## MDX/Code Formatting\nWhen including code blocks in your response:\n- Never nest fenced code blocks (triple backticks) inside other fenced code blocks.\n- If you need to show MDX source that itself contains code blocks, use indented code blocks or escape the inner backticks.\n- All output must be valid MDX that renders correctly.\n\n## Internal Links\nEach context chunk includes a \"URL:\" line with the pre-computed page URL. Use it directly when linking:\n- Format links as markdown: [Page Title](/slug/).\n- Never expose raw file paths like \"/app/.../page.tsx\" to the user.\n- Never include route group segments in parentheses, like \"(site)\", in any link - they are internal directory names and do not exist in real URLs. Use \"/code/\" not \"/(site)/code/\".\n- Do NOT add a \"Related Pages\" section at the end - sources are shown separately by the UI.\n\n## Greetings & Small Talk\nIf the user sends a greeting or non-documentation question, respond briefly and ask how you can help with the documentation.`;\n\n// Maps provider/stream failures to fixed visitor-safe messages. Raw provider\n// errors can include API-key fragments, org ids, and billing URLs, so only\n// these strings ever reach the client; the full error goes to the server log.\nfunction friendlyLLMError(error: unknown): string {\n const raw = error instanceof Error ? error.message : String(error);\n if (/\\b401\\b|api.?key|authenticat|unauthorized/i.test(raw)) {\n return \"The assistant's model provider rejected its API key. If you are the site owner, check the server logs and your API key configuration.\";\n }\n if (/\\b429\\b|quota|rate.?limit|billing/i.test(raw)) {\n return \"The assistant's model provider is rate limiting requests right now. Please try again in a moment.\";\n }\n if (\n /timed?.?out|ETIMEDOUT|ECONNRESET|ENOTFOUND|fetch failed|network/i.test(raw)\n ) {\n return \"The assistant could not reach its model provider. Please try again.\";\n }\n return \"The assistant hit an unexpected error. Please try again.\";\n}\n\n// LangChain + the MCP SDK require the Node.js runtime (not edge).\nexport const runtime = \"nodejs\";\n// Safety net for the streaming function (Vercel-only; ignored elsewhere).\n// The heartbeat below, not this value, is what prevents proxy 524 timeouts.\nexport const maxDuration = 60;\n\nexport async function POST(req: Request) {\n if (!(await isRagRequestAuthorized(req))) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n const { allowed, retryAfter } = rateLimit(req);\n if (!allowed) {\n return NextResponse.json(\n { error: \"Too many requests\" },\n { status: 429, headers: { \"Retry-After\": String(retryAfter) } },\n );\n }\n\n // Validate the request up front so genuine client/config errors still return\n // real status codes before we commit to a 200 streaming response.\n let body: unknown;\n try {\n body = await readJsonBody(req, MAX_RAG_REQUEST_BYTES);\n } catch (error: unknown) {\n if (req.signal.aborted) throw error;\n if (error instanceof RequestTooLargeError) {\n return NextResponse.json(\n { error: \"Request body too large\" },\n { status: 413 },\n );\n }\n return NextResponse.json({ error: \"Invalid JSON body\" }, { status: 400 });\n }\n\n const parsed = ragSchema.safeParse(body);\n if (!parsed.success) {\n return NextResponse.json(\n { error: \"Invalid input\", details: parsed.error.issues },\n { status: 400 },\n );\n }\n const { question, history } = parsed.data;\n\n let llmConfig;\n try {\n llmConfig = getLLMConfig();\n } catch (error: unknown) {\n // Setup details (provider names, env var names) stay in the server log.\n console.error(\"[doccupine] LLM configuration error:\", error);\n return NextResponse.json(\n {\n error:\n \"The AI assistant is not configured on this site. If you are the site owner, check the server logs.\",\n },\n { status: 500 },\n );\n }\n\n const encoder = new TextEncoder();\n let heartbeat: ReturnType | null = null;\n let streamClosed = false;\n const workController = new AbortController();\n const abortWork = () => workController.abort(req.signal.reason);\n req.signal.addEventListener(\"abort\", abortWork, { once: true });\n if (req.signal.aborted) abortWork();\n const { signal } = workController;\n\n // Return the streaming response immediately and do ALL slow work (indexing,\n // search, model streaming) inside start(). This flushes headers + a first\n // byte right away so edge proxies never hit their time-to-first-byte timeout.\n const readableStream = new ReadableStream({\n async start(controller) {\n const safeEnqueue = (payload: string) => {\n if (streamClosed) return;\n try {\n controller.enqueue(encoder.encode(payload));\n } catch {\n // Stream already closed or cancelled - stop emitting.\n streamClosed = true;\n workController.abort();\n if (heartbeat) clearInterval(heartbeat);\n }\n };\n\n // Keep the connection alive while the index and model warm up.\n heartbeat = setInterval(() => safeEnqueue(`: keep-alive\\n\\n`), 15000);\n\n try {\n // First byte, before any slow work - satisfies the proxy TTFB window.\n safeEnqueue(`: connected\\n\\n`);\n\n // Ensure docs are indexed (loads precomputed embeddings when present).\n await ensureDocsIndex(false, signal);\n signal.throwIfAborted();\n\n // Use MCP search_docs tool to find relevant documentation\n const searchResults = await searchDocs(question, 6, signal);\n signal.throwIfAborted();\n\n // Build context from search results\n const context = searchResults\n .map(({ chunk, score }) => {\n const slug = chunk.uri.replace(\"docs://\", \"\").replace(/^\\/+/, \"\");\n const url = slug ? `/${slug}/` : \"/\";\n return `File: ${chunk.path}\\nURL: ${url}\\nScore: ${score.toFixed(3)}\\n----\\n${chunk.text}`;\n })\n .join(\"\\n\\n================\\n\\n\");\n\n // Build metadata from MCP search results and send it before the answer.\n const indexStatus = getIndexStatus();\n const metadata = {\n sources: searchResults.map(({ chunk, score }) => ({\n id: chunk.id,\n path: chunk.path,\n uri: chunk.uri,\n score,\n })),\n chunkCount: indexStatus.chunkCount,\n };\n safeEnqueue(\n `data: ${JSON.stringify({ type: \"metadata\", data: metadata })}\\n\\n`,\n );\n\n // Assemble the prompt, including conversation history for multi-turn.\n const prompt: {\n role: \"system\" | \"user\" | \"assistant\";\n content: string;\n }[] = [\n {\n role: \"system\" as const,\n content: systemContext,\n },\n ];\n if (history && history.length > 0) {\n for (const msg of history) {\n prompt.push({\n role: msg.role,\n content: msg.content,\n });\n }\n }\n prompt.push({\n role: \"user\" as const,\n content: `Question: ${question}\\n\\nContext:\\n${context}`,\n });\n\n // Create chat model and stream the response.\n const llm = createChatModel(llmConfig);\n const stream = await llm.stream(prompt, { signal });\n for await (const chunk of stream) {\n signal.throwIfAborted();\n const content = chunk?.content || \"\";\n if (content) {\n safeEnqueue(\n `data: ${JSON.stringify({ type: \"content\", data: content })}\\n\\n`,\n );\n }\n }\n\n signal.throwIfAborted();\n safeEnqueue(`data: ${JSON.stringify({ type: \"done\" })}\\n\\n`);\n } catch (error: unknown) {\n if (signal.aborted) return;\n console.error(\"[doccupine] RAG stream error:\", error);\n safeEnqueue(\n `data: ${JSON.stringify({ type: \"error\", data: friendlyLLMError(error) })}\\n\\n`,\n );\n } finally {\n if (heartbeat) clearInterval(heartbeat);\n streamClosed = true;\n req.signal.removeEventListener(\"abort\", abortWork);\n try {\n controller.close();\n } catch {\n // Already closed.\n }\n }\n },\n cancel() {\n streamClosed = true;\n workController.abort();\n if (heartbeat) clearInterval(heartbeat);\n },\n });\n\n return new Response(readableStream, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n },\n });\n}\n\nexport async function GET(req: Request) {\n if (!(await isRagRequestAuthorized(req))) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n const status = getIndexStatus();\n return NextResponse.json({\n ready: status.ready,\n chunks: status.chunkCount,\n reason: status.reason,\n });\n}\n";