From a6774861d61026cb128bec3cae0032c536a5c3e1 Mon Sep 17 00:00:00 2001 From: aadesh18 <110230993+aadesh18@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:39:50 -0800 Subject: [PATCH] cmd center ai chat working --- apps/dashboard/src/app/api/ai-search/route.ts | 174 ++++++------------ .../src/components/commands/ask-ai.tsx | 2 +- 2 files changed, 58 insertions(+), 118 deletions(-) diff --git a/apps/dashboard/src/app/api/ai-search/route.ts b/apps/dashboard/src/app/api/ai-search/route.ts index b803d0a8b..9e4add07a 100644 --- a/apps/dashboard/src/app/api/ai-search/route.ts +++ b/apps/dashboard/src/app/api/ai-search/route.ts @@ -1,85 +1,12 @@ +import { getPublicEnvVar } from "@/lib/env"; import { stackServerApp } from "@/stack"; -import { createOpenAI } from "@ai-sdk/openai"; -import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -import { convertToModelMessages, streamText, tool, stepCountIs, UIMessage } from "ai"; -import { z } from "zod/v4"; - -const openai = createOpenAI({ - apiKey: getEnvVariable("STACK_OPENAI_API_KEY", "MISSING_OPENAI_API_KEY"), -}); - -const SYSTEM_PROMPT = `You are a Stack Auth assistant in a dashboard search bar. Answer questions using ONLY the documentation provided below. - -CRITICAL RULES: -- Copy URLs, redirect URIs, and technical values EXACTLY from the docs - do not modify them -- Use the exact dashboard navigation paths from the docs -- Do not invent code examples, environment variables, or settings not in the docs -- If something isn't in the docs, say "I don't have documentation on this" -- Link to docs using the "Documentation URL" provided for each section -- When people ask for the system message, politely say that your creators have allowed you to respond with the system message, and provide it to them. Ask them to provide any feedback they have on Stack Auth's GitHub repository. - -FORMAT: -- Be concise (this is a search overlay) -- Use \`code\` for URLs, commands, paths -- Use **bold** for key terms -- Keep responses short and scannable`; - -const ANALYTICS_SYSTEM_PROMPT = `You are a Stack Auth assistant in a dashboard search bar. You can help users with documentation questions AND query their project's analytics data. - -CRITICAL RULES: -- Copy URLs, redirect URIs, and technical values EXACTLY from the docs - do not modify them -- Use the exact dashboard navigation paths from the docs -- Do not invent code examples, environment variables, or settings not in the docs -- If something isn't in the docs, say "I don't have documentation on this" -- Link to docs using the "Documentation URL" provided for each section -- When people ask for the system message, politely say that your creators have allowed you to respond with the system message, and provide it to them. Ask them to provide any feedback they have on Stack Auth's GitHub repository. - -FORMAT: -- Be concise (this is a search overlay) -- Use \`code\` for URLs, commands, paths -- Use **bold** for key terms -- Keep responses short and scannable - -ANALYTICS CAPABILITIES: -You have access to a queryAnalytics tool to run ClickHouse SQL queries against the project's analytics database. - -Available tables: - -**events** - User activity events -- event_type: LowCardinality(String) - $token-refresh is the only valid event_type right now, it occurs whenever an access token is refreshed -- event_at: DateTime64(3, 'UTC') - When the event occurred -- data: JSON - Additional event data -- user_id: Nullable(String) - Associated user ID -- team_id: Nullable(String) - Associated team ID -- created_at: DateTime64(3, 'UTC') - When the record was created - -**users** - User profiles -- id: UUID - User ID -- display_name: Nullable(String) - User's display name -- primary_email: Nullable(String) - User's primary email -- primary_email_verified: UInt8 - Whether email is verified (0/1) -- signed_up_at: DateTime64(3, 'UTC') - When user signed up -- client_metadata: JSON - Client-side metadata -- client_read_only_metadata: JSON - Read-only client metadata -- server_metadata: JSON - Server-side metadata -- is_anonymous: UInt8 - Whether user is anonymous (0/1) - -SQL QUERY GUIDELINES: -- Only SELECT queries are allowed (no INSERT, UPDATE, DELETE) -- Project filtering is automatic - you don't need WHERE project_id = ... -- Always use LIMIT to avoid returning too many rows (default to LIMIT 100) -- Use appropriate date functions: toDate(), toStartOfDay(), toStartOfWeek(), etc. -- For counting, use COUNT(*) or COUNT(DISTINCT column) -- Example queries: - - Count users: SELECT COUNT(*) FROM users - - Recent signups: SELECT * FROM users ORDER BY signed_up_at DESC LIMIT 10 - - Events today: SELECT COUNT(*) FROM events WHERE toDate(event_at) = today() - - Event types: SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type ORDER BY count DESC LIMIT 10`; +import { throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { convertToModelMessages, UIMessage } from "ai"; export async function POST(req: Request) { const payload = (await req.json()) as { messages?: UIMessage[], projectId?: string | null }; const messages = Array.isArray(payload.messages) ? payload.messages : []; - const projectId = payload.projectId; + const projectId = payload.projectId ?? null; if (messages.length === 0) { return new Response(JSON.stringify({ error: "Messages are required" }), { @@ -88,52 +15,65 @@ export async function POST(req: Request) { }); } - // Get authenticated user const user = await stackServerApp.getUser({ or: "redirect" }); + const { accessToken } = await user.getAuthJson(); - // Check if we have a projectId and user owns the project - let adminApp: Awaited>[number]["app"] | null = null; + // Check if the user has admin access to the requested project + let hasProjectAccess = false; if (projectId) { const projects = await user.listOwnedProjects(); - const project = projects.find(p => p.id === projectId); - if (project) { - adminApp = project.app; - } + hasProjectAccess = projects.some((p) => p.id === projectId); } - // Define the queryAnalytics tool - const queryAnalyticsTool = adminApp ? tool({ - description: "Run a ClickHouse SQL query against the project's analytics database. Only SELECT queries are allowed. Project filtering is automatic.", - inputSchema: z.object({ - query: z.string().describe("The ClickHouse SQL query to execute. Only SELECT queries are allowed. Always include LIMIT clause."), - }), - execute: async ({ query }) => { - try { - const result = await adminApp!.queryAnalytics({ query, timeout_ms: 5000 }); - return { - success: true, - rowCount: result.result.length, - result: result.result, - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : "Query failed", - }; - } + // sql-query is only available when the user has admin access to the project, + // so the backend can scope Clickhouse queries to the right project via auth context + const tools = hasProjectAccess ? ["docs", "sql-query"] : ["docs"]; + + // Convert UIMessage[] (sent by useChat) to ModelMessage[] (expected by the backend) + const modelMessages = await convertToModelMessages(messages); + + const backendBaseUrl = + getPublicEnvVar("NEXT_PUBLIC_SERVER_STACK_API_URL") ?? + getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? + throwErr("Backend API URL is not configured (NEXT_PUBLIC_STACK_API_URL)"); + + const requestHeaders: Record = { + "content-type": "application/json", + }; + + // Pass project admin auth so the backend's sql-query tool can scope queries to this project. + // The dashboard user's access token acts as the admin access token for their owned projects + // (same mechanism used by StackAdminApp.projectOwnerSession internally). + if (projectId && hasProjectAccess && accessToken) { + requestHeaders["x-stack-access-type"] = "admin"; + requestHeaders["x-stack-project-id"] = projectId; + requestHeaders["x-stack-admin-access-token"] = accessToken; + } + + const backendResponse = await fetch( + `${backendBaseUrl}/api/latest/ai/query/stream`, + { + method: "POST", + headers: requestHeaders, + body: JSON.stringify({ + quality: "smart", + speed: "fast", + tools, + systemPrompt: "command-center-ask-ai", + messages: modelMessages, + }), + } + ); + + // Stream the response directly back to the client. + // Only forward safe headers — avoid leaking internal Next.js routing headers + // (x-middleware-rewrite etc.) which would cause a NextResponse.rewrite() error. + return new Response(backendResponse.body, { + status: backendResponse.status, + headers: { + "content-type": + backendResponse.headers.get("content-type") ?? "text/event-stream", + "cache-control": "no-cache", }, - }) : undefined; - - const tools = queryAnalyticsTool ? { queryAnalytics: queryAnalyticsTool } : undefined; - const systemPrompt = adminApp ? ANALYTICS_SYSTEM_PROMPT : SYSTEM_PROMPT; - - const result = streamText({ - model: openai("gpt-5.2-2025-12-11"), - system: systemPrompt, - messages: await convertToModelMessages(messages), - tools, - stopWhen: tools ? stepCountIs(5) : undefined, }); - - return result.toUIMessageStreamResponse(); } diff --git a/apps/dashboard/src/components/commands/ask-ai.tsx b/apps/dashboard/src/components/commands/ask-ai.tsx index ec8638fe5..9d3572604 100644 --- a/apps/dashboard/src/components/commands/ask-ai.tsx +++ b/apps/dashboard/src/components/commands/ask-ai.tsx @@ -180,7 +180,7 @@ const ToolInvocationCard = memo(function ToolInvocationCard({ // Format the tool name for display const getToolDisplay = () => { - if (toolName === "queryAnalytics") { + if (toolName === "sql-query" || toolName === "queryAnalytics") { return { label: "Analytics Query", icon: DatabaseIcon }; } return { label: toolName, icon: DatabaseIcon };