route.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { readFile } from "fs/promises";
  2. import { join } from "path";
  3. import { convertToModelMessages, stepCountIs, streamText } from "ai";
  4. import type { ModelMessage, UIMessage } from "ai";
  5. import { createBashTool } from "bash-tool";
  6. import { headers } from "next/headers";
  7. import { allDocsPages } from "@/lib/docs-navigation";
  8. import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
  9. import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
  10. export const maxDuration = 60;
  11. const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
  12. const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render, a library for AI-generated UI with guardrails.
  13. GitHub repository: https://github.com/vercel-labs/json-render
  14. Documentation: https://json-render.dev/docs
  15. npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate
  16. Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
  17. You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
  18. When answering questions:
  19. - Use the bash tool to list files (ls /workspace/docs/) or search for content (grep -r "keyword" /workspace/docs/)
  20. - Use the readFile tool to read specific documentation pages (e.g. readFile with path "/workspace/docs/index.md")
  21. - 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
  22. - Always base your answers on the actual documentation content
  23. - Be concise and accurate
  24. - If the docs don't cover a topic, say so honestly
  25. - Do NOT include source references or file paths in your response
  26. - Do NOT use emojis in your responses`;
  27. async function loadDocsFiles(): Promise<Record<string, string>> {
  28. const files: Record<string, string> = {};
  29. const results = await Promise.allSettled(
  30. allDocsPages.map(async (page) => {
  31. const slug =
  32. page.href === "/docs" ? "" : page.href.replace(/^\/docs\/?/, "");
  33. const filePath = slug
  34. ? join(
  35. process.cwd(),
  36. "app",
  37. "(main)",
  38. "docs",
  39. ...slug.split("/"),
  40. "page.mdx",
  41. )
  42. : join(process.cwd(), "app", "(main)", "docs", "page.mdx");
  43. const raw = await readFile(filePath, "utf-8");
  44. const md = mdxToCleanMarkdown(raw);
  45. const fileName = slug ? `/docs/${slug}.md` : "/docs/index.md";
  46. return { fileName, md };
  47. }),
  48. );
  49. for (const result of results) {
  50. if (result.status === "fulfilled") {
  51. files[result.value.fileName] = result.value.md;
  52. }
  53. }
  54. return files;
  55. }
  56. function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
  57. if (messages.length === 0) return messages;
  58. return messages.map((message, index) => {
  59. if (index === messages.length - 1) {
  60. return {
  61. ...message,
  62. providerOptions: {
  63. ...message.providerOptions,
  64. anthropic: { cacheControl: { type: "ephemeral" } },
  65. },
  66. };
  67. }
  68. return message;
  69. });
  70. }
  71. export async function POST(req: Request) {
  72. const headersList = await headers();
  73. const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
  74. const [minuteResult, dailyResult] = await Promise.all([
  75. minuteRateLimit.limit(ip),
  76. dailyRateLimit.limit(ip),
  77. ]);
  78. if (!minuteResult.success || !dailyResult.success) {
  79. const isMinuteLimit = !minuteResult.success;
  80. return new Response(
  81. JSON.stringify({
  82. error: "Rate limit exceeded",
  83. message: isMinuteLimit
  84. ? "Too many requests. Please wait a moment before trying again."
  85. : "Daily limit reached. Please try again tomorrow.",
  86. }),
  87. {
  88. status: 429,
  89. headers: { "Content-Type": "application/json" },
  90. },
  91. );
  92. }
  93. const { messages }: { messages: UIMessage[] } = await req.json();
  94. const docsFiles = await loadDocsFiles();
  95. const {
  96. tools: { bash, readFile },
  97. } = await createBashTool({ files: docsFiles });
  98. const result = streamText({
  99. model: DEFAULT_MODEL,
  100. system: SYSTEM_PROMPT,
  101. messages: await convertToModelMessages(messages),
  102. stopWhen: stepCountIs(5),
  103. tools: {
  104. bash,
  105. readFile,
  106. },
  107. prepareStep: ({ messages: stepMessages }) => ({
  108. messages: addCacheControl(stepMessages),
  109. }),
  110. });
  111. return result.toUIMessageStreamResponse();
  112. }