generate-v4.ts255 lines · main
| 1 | import pgMeta from '@supabase/pg-meta' |
| 2 | import type { JwtPayload } from '@supabase/supabase-js' |
| 3 | import { safeValidateUIMessages } from 'ai' |
| 4 | import { IS_PLATFORM } from 'common' |
| 5 | import type { NextApiRequest, NextApiResponse } from 'next' |
| 6 | import z from 'zod' |
| 7 | |
| 8 | import { executeSql } from '@/data/sql/execute-sql-query' |
| 9 | import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi' |
| 10 | import { getOrgAIDetails, getProjectAIDetails } from '@/lib/ai/ai-details' |
| 11 | import { isTracingAllowed } from '@/lib/ai/braintrust-logger' |
| 12 | import { generateAssistantResponse } from '@/lib/ai/generate-assistant-response' |
| 13 | import { getModel } from '@/lib/ai/model' |
| 14 | import { |
| 15 | DEFAULT_ASSISTANT_ADVANCE_MODEL_ID, |
| 16 | DEFAULT_ASSISTANT_BASE_MODEL_ID, |
| 17 | getAssistantModelEntry, |
| 18 | isAssistantBaseModelId, |
| 19 | isKnownAssistantModelId, |
| 20 | type AssistantModelId, |
| 21 | } from '@/lib/ai/model.utils' |
| 22 | import { getTools } from '@/lib/ai/tools' |
| 23 | import apiWrapper from '@/lib/api/apiWrapper' |
| 24 | import { executeQuery } from '@/lib/api/self-hosted/query' |
| 25 | import { getURL } from '@/lib/helpers' |
| 26 | |
| 27 | export const maxDuration = 120 |
| 28 | |
| 29 | export const config = { |
| 30 | api: { |
| 31 | bodyParser: { |
| 32 | sizeLimit: '5mb', |
| 33 | }, |
| 34 | }, |
| 35 | } |
| 36 | |
| 37 | async function handler(req: NextApiRequest, res: NextApiResponse, claims?: JwtPayload) { |
| 38 | const { method } = req |
| 39 | |
| 40 | switch (method) { |
| 41 | case 'POST': |
| 42 | return handlePost(req, res, claims) |
| 43 | default: |
| 44 | res.setHeader('Allow', ['POST']) |
| 45 | res.status(405).json({ |
| 46 | data: null, |
| 47 | error: { message: `Method ${method} Not Allowed` }, |
| 48 | }) |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | const wrapper = (req: NextApiRequest, res: NextApiResponse) => |
| 53 | apiWrapper(req, res, handler, { withAuth: true }) |
| 54 | |
| 55 | export default wrapper |
| 56 | |
| 57 | const requestBodySchema = z.object({ |
| 58 | messages: z.array(z.any()), |
| 59 | projectRef: z.string(), |
| 60 | connectionString: z.string(), |
| 61 | schema: z.string().optional(), |
| 62 | table: z.string().optional(), |
| 63 | chatId: z.string().optional(), |
| 64 | chatName: z.string().optional(), |
| 65 | orgSlug: z.string().optional(), |
| 66 | model: z.string().optional(), |
| 67 | }) |
| 68 | |
| 69 | async function handlePost(req: NextApiRequest, res: NextApiResponse, claims?: JwtPayload) { |
| 70 | const authorization = req.headers.authorization |
| 71 | const accessToken = authorization?.replace('Bearer ', '') |
| 72 | |
| 73 | if (IS_PLATFORM && !accessToken) { |
| 74 | return res.status(401).json({ error: 'Authorization token is required' }) |
| 75 | } |
| 76 | |
| 77 | const userId = claims?.sub |
| 78 | |
| 79 | const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body |
| 80 | const { data, error: parseError } = requestBodySchema.safeParse(body) |
| 81 | |
| 82 | if (parseError) { |
| 83 | return res.status(400).json({ error: 'Invalid request body', issues: parseError.issues }) |
| 84 | } |
| 85 | |
| 86 | const { |
| 87 | messages: rawMessages, |
| 88 | projectRef, |
| 89 | connectionString, |
| 90 | orgSlug, |
| 91 | chatId, |
| 92 | chatName, |
| 93 | model: rawRequestedModel, |
| 94 | } = data |
| 95 | |
| 96 | const requestedModel: AssistantModelId | undefined = |
| 97 | rawRequestedModel && isKnownAssistantModelId(rawRequestedModel) ? rawRequestedModel : undefined |
| 98 | |
| 99 | const messagesValidation = await safeValidateUIMessages({ |
| 100 | messages: rawMessages, |
| 101 | }) |
| 102 | if (!messagesValidation.success) { |
| 103 | return res.status(400).json({ |
| 104 | error: 'Invalid request body', |
| 105 | message: messagesValidation.error.message, |
| 106 | }) |
| 107 | } |
| 108 | const messages = messagesValidation.data |
| 109 | |
| 110 | let aiOptInLevel: AiOptInLevel = 'disabled' |
| 111 | let hasAccessToAdvanceModel = false |
| 112 | let orgHasHipaaAddon: boolean | undefined |
| 113 | let projectIsSensitive: boolean | undefined |
| 114 | let projectRegion: string | undefined |
| 115 | let orgId: number | undefined |
| 116 | let planId: string | undefined |
| 117 | |
| 118 | if (!IS_PLATFORM) { |
| 119 | aiOptInLevel = 'schema' |
| 120 | hasAccessToAdvanceModel = true |
| 121 | } |
| 122 | |
| 123 | if (IS_PLATFORM && orgSlug && authorization && projectRef) { |
| 124 | try { |
| 125 | const [orgDetails, projectDetails] = await Promise.all([ |
| 126 | getOrgAIDetails({ orgSlug, authorization }), |
| 127 | getProjectAIDetails({ projectRef, authorization }), |
| 128 | ]) |
| 129 | |
| 130 | aiOptInLevel = orgDetails.aiOptInLevel |
| 131 | hasAccessToAdvanceModel = orgDetails.hasAccessToAdvanceModel |
| 132 | orgHasHipaaAddon = orgDetails.hasHipaaAddon |
| 133 | orgId = orgDetails.orgId |
| 134 | planId = orgDetails.planId |
| 135 | projectIsSensitive = projectDetails.isSensitive |
| 136 | projectRegion = projectDetails.region |
| 137 | } catch (error) { |
| 138 | return res.status(400).json({ |
| 139 | error: 'There was an error fetching your organization details', |
| 140 | }) |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | const envThrottled = process.env.IS_THROTTLED !== 'false' |
| 145 | |
| 146 | let effectiveModel: AssistantModelId = requestedModel ?? DEFAULT_ASSISTANT_ADVANCE_MODEL_ID |
| 147 | if (!hasAccessToAdvanceModel || (envThrottled && !isAssistantBaseModelId(effectiveModel))) { |
| 148 | effectiveModel = DEFAULT_ASSISTANT_BASE_MODEL_ID |
| 149 | } |
| 150 | |
| 151 | const { |
| 152 | modelParams, |
| 153 | error: modelError, |
| 154 | systemProviderOptions, |
| 155 | } = await getModel({ |
| 156 | provider: 'openai', |
| 157 | modelEntry: getAssistantModelEntry(effectiveModel), |
| 158 | }) |
| 159 | |
| 160 | if (modelError) { |
| 161 | return res.status(500).json({ error: modelError.message }) |
| 162 | } |
| 163 | |
| 164 | try { |
| 165 | const abortController = new AbortController() |
| 166 | req.on('close', () => abortController.abort()) |
| 167 | req.on('aborted', () => abortController.abort()) |
| 168 | |
| 169 | const tools = await getTools({ |
| 170 | projectRef, |
| 171 | connectionString, |
| 172 | authorization, |
| 173 | aiOptInLevel, |
| 174 | accessToken, |
| 175 | baseUrl: getURL(), |
| 176 | }) |
| 177 | |
| 178 | // Get a list of all schemas to add to context |
| 179 | const getSchemas = async (): Promise<string> => { |
| 180 | const pgMetaSchemasList = pgMeta.schemas.list() |
| 181 | type Schemas = z.infer<(typeof pgMetaSchemasList)['zod']> |
| 182 | |
| 183 | const { result: schemas } = await executeSql<Schemas>( |
| 184 | { |
| 185 | projectRef, |
| 186 | connectionString, |
| 187 | sql: pgMetaSchemasList.sql, |
| 188 | }, |
| 189 | undefined, |
| 190 | { |
| 191 | 'Content-Type': 'application/json', |
| 192 | ...(authorization && { Authorization: authorization }), |
| 193 | }, |
| 194 | IS_PLATFORM ? undefined : executeQuery |
| 195 | ) |
| 196 | |
| 197 | return schemas?.length > 0 |
| 198 | ? `The available database schema names are: ${JSON.stringify(schemas)}` |
| 199 | : "You don't have access to any schemas." |
| 200 | } |
| 201 | |
| 202 | const result = await generateAssistantResponse({ |
| 203 | messages, |
| 204 | ...modelParams, |
| 205 | tools, |
| 206 | aiOptInLevel, |
| 207 | getSchemas: aiOptInLevel !== 'disabled' ? getSchemas : undefined, |
| 208 | projectRef, |
| 209 | chatId, |
| 210 | chatName, |
| 211 | allowTracing: isTracingAllowed({ |
| 212 | orgHasHipaaAddon, |
| 213 | projectIsSensitive, |
| 214 | projectRegion, |
| 215 | }), |
| 216 | userId, |
| 217 | orgId, |
| 218 | planId, |
| 219 | requestedModel, |
| 220 | systemProviderOptions, |
| 221 | abortSignal: abortController.signal, |
| 222 | onSpanCreated: (spanId) => { |
| 223 | res.setHeader('x-braintrust-span-id', spanId) |
| 224 | }, |
| 225 | }) |
| 226 | |
| 227 | result.pipeUIMessageStreamToResponse(res, { |
| 228 | sendReasoning: true, |
| 229 | headers: { 'Content-Encoding': 'none' }, |
| 230 | onError: (error) => { |
| 231 | console.error('Assistant stream error:', error) |
| 232 | |
| 233 | if (error == null) { |
| 234 | return 'unknown error' |
| 235 | } |
| 236 | |
| 237 | if (typeof error === 'string') { |
| 238 | return error |
| 239 | } |
| 240 | |
| 241 | if (error instanceof Error) { |
| 242 | return error.message |
| 243 | } |
| 244 | |
| 245 | return JSON.stringify(error) |
| 246 | }, |
| 247 | }) |
| 248 | } catch (error) { |
| 249 | console.error('Error in handlePost:', error) |
| 250 | if (error instanceof Error) { |
| 251 | return res.status(500).json({ message: error.message }) |
| 252 | } |
| 253 | return res.status(500).json({ message: 'An unexpected error occurred.' }) |
| 254 | } |
| 255 | } |