chore: import upstream snapshot with attribution
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { convertToModelMessages, stepCountIs, streamText } from "ai";
|
||||
import type { ModelMessage, UIMessage } from "ai";
|
||||
import { createBashTool } from "bash-tool";
|
||||
import { docs } from "@/lib/docs";
|
||||
import { readArticleBySlug } from "@/lib/articles";
|
||||
import {
|
||||
docsChatDailyRateLimit,
|
||||
docsChatMinuteRateLimit,
|
||||
isDocsChatRateLimitConfigured,
|
||||
} from "@/lib/rate-limit";
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
|
||||
|
||||
const SYSTEM_PROMPT = `You are a helpful documentation assistant for Zero, a pre-1 experimental programming language project exploring agent-first design. Zero intentionally makes breaking changes while searching for the language, library, and tooling patterns that work best for agents. Security vulnerabilities should be expected; Zero should be run or developed only in safe, non-production environments.
|
||||
|
||||
GitHub repository: https://github.com/vercel-labs/zerolang
|
||||
Documentation: https://zerolang.ai
|
||||
|
||||
You have access to the full Zero documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory. The file layout matches each page's URL slug — e.g. /workspace/index.md is the home, /workspace/getting-started.md is /getting-started, /workspace/modules/parse.md is /modules/parse.
|
||||
|
||||
When answering questions:
|
||||
- Use the bash tool to list files (ls /workspace/) or search for content (grep -r "keyword" /workspace/)
|
||||
- Use the readFile tool to read specific documentation pages (e.g. readFile with path "/workspace/getting-started.md")
|
||||
- Do NOT use bash to write, create, modify, or delete files (no tee, cat >, sed -i, echo >, cp, mv, rm, mkdir, touch, etc.) — you are read-only
|
||||
- Always base your answers on the actual documentation content
|
||||
- Be concise and accurate
|
||||
- If the docs don't cover a topic, say so honestly
|
||||
- Do NOT include source references or file paths in your response
|
||||
- Do NOT use emojis in your responses
|
||||
- When showing Zero projection syntax, use \`\`\`zero fenced blocks and say it is a human-readable projection of the graph. When showing shell commands, use \`\`\`sh.`;
|
||||
|
||||
type DocsFiles = Record<string, string>;
|
||||
|
||||
let cachedDocsFiles: DocsFiles | null = null;
|
||||
|
||||
async function loadDocsFiles(): Promise<DocsFiles> {
|
||||
if (cachedDocsFiles) return cachedDocsFiles;
|
||||
|
||||
const files: DocsFiles = {};
|
||||
const results = await Promise.allSettled(
|
||||
docs.map(async (doc) => {
|
||||
const md = await readArticleBySlug(doc.slug);
|
||||
if (!md) return null;
|
||||
const fileName = doc.path === "/" ? "/index.md" : `${doc.path}.md`;
|
||||
return { fileName: fileName.replace(/^\//, ""), md };
|
||||
}),
|
||||
);
|
||||
for (const result of results) {
|
||||
if (result.status === "fulfilled" && result.value) {
|
||||
files[result.value.fileName] = result.value.md;
|
||||
}
|
||||
}
|
||||
cachedDocsFiles = files;
|
||||
return files;
|
||||
}
|
||||
|
||||
function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
|
||||
if (messages.length === 0) return messages;
|
||||
return messages.map((message, index) => {
|
||||
if (index === messages.length - 1) {
|
||||
return {
|
||||
...message,
|
||||
providerOptions: {
|
||||
...message.providerOptions,
|
||||
anthropic: { cacheControl: { type: "ephemeral" } },
|
||||
},
|
||||
};
|
||||
}
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
function getRateLimitIdentifier(req: Request): string {
|
||||
const forwardedFor = req.headers.get("x-forwarded-for");
|
||||
const realIp = req.headers.get("x-real-ip");
|
||||
return forwardedFor?.split(",")[0]?.trim() || realIp?.trim() || "anonymous";
|
||||
}
|
||||
|
||||
async function rateLimitRequest(req: Request): Promise<NextResponse | null> {
|
||||
if (!isDocsChatRateLimitConfigured()) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Chat rate limiting is not configured",
|
||||
message: "Set KV_REST_API_URL and KV_REST_API_TOKEN to enable docs chat.",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
const identifier = getRateLimitIdentifier(req);
|
||||
let minuteResult;
|
||||
let dailyResult;
|
||||
try {
|
||||
[minuteResult, dailyResult] = await Promise.all([
|
||||
docsChatMinuteRateLimit.limit(identifier),
|
||||
docsChatDailyRateLimit.limit(identifier),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Docs chat rate limiting failed", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Chat rate limiting unavailable",
|
||||
message: "Docs chat is temporarily unavailable because rate limiting could not be applied.",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
if (minuteResult.success && dailyResult.success) return null;
|
||||
|
||||
const isMinuteLimit = !minuteResult.success;
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Rate limit exceeded",
|
||||
message: isMinuteLimit
|
||||
? "Too many requests. Please wait a moment before trying again."
|
||||
: "Daily limit reached. Please try again tomorrow.",
|
||||
},
|
||||
{ status: 429 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
if (!process.env.AI_GATEWAY_API_KEY && !process.env.VERCEL_OIDC_TOKEN) {
|
||||
return NextResponse.json(
|
||||
{ error: "Chat is not configured. Set AI_GATEWAY_API_KEY or configure deployment OIDC." },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
const rateLimitResponse = await rateLimitRequest(req);
|
||||
if (rateLimitResponse) return rateLimitResponse;
|
||||
|
||||
let body: { messages?: UIMessage[] } | null;
|
||||
try {
|
||||
body = (await req.json()) as { messages?: UIMessage[] };
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
||||
|
||||
const docsFiles = await loadDocsFiles();
|
||||
const {
|
||||
tools: { bash, readFile },
|
||||
} = await createBashTool({ files: docsFiles });
|
||||
|
||||
const result = streamText({
|
||||
model: DEFAULT_MODEL,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: await convertToModelMessages(messages),
|
||||
stopWhen: stepCountIs(5),
|
||||
tools: { bash, readFile },
|
||||
prepareStep: ({ messages: stepMessages }) => ({
|
||||
messages: addCacheControl(stepMessages),
|
||||
}),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
Reference in New Issue
Block a user