| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- import pgMeta from '@supabase/pg-meta'
- import type { JwtPayload } from '@supabase/supabase-js'
- import { safeValidateUIMessages } from 'ai'
- import { IS_PLATFORM } from 'common'
- import type { NextApiRequest, NextApiResponse } from 'next'
- import z from 'zod'
- import { executeSql } from '@/data/sql/execute-sql-query'
- import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
- import { getOrgAIDetails, getProjectAIDetails } from '@/lib/ai/ai-details'
- import { isTracingAllowed } from '@/lib/ai/braintrust-logger'
- import { generateAssistantResponse } from '@/lib/ai/generate-assistant-response'
- import { getModel } from '@/lib/ai/model'
- import {
- DEFAULT_ASSISTANT_ADVANCE_MODEL_ID,
- DEFAULT_ASSISTANT_BASE_MODEL_ID,
- getAssistantModelEntry,
- isAssistantBaseModelId,
- isKnownAssistantModelId,
- type AssistantModelId,
- } from '@/lib/ai/model.utils'
- import { getTools } from '@/lib/ai/tools'
- import apiWrapper from '@/lib/api/apiWrapper'
- import { executeQuery } from '@/lib/api/self-hosted/query'
- import { getURL } from '@/lib/helpers'
- export const maxDuration = 120
- export const config = {
- api: {
- bodyParser: {
- sizeLimit: '5mb',
- },
- },
- }
- async function handler(req: NextApiRequest, res: NextApiResponse, claims?: JwtPayload) {
- const { method } = req
- switch (method) {
- case 'POST':
- return handlePost(req, res, claims)
- default:
- res.setHeader('Allow', ['POST'])
- res.status(405).json({
- data: null,
- error: { message: `Method ${method} Not Allowed` },
- })
- }
- }
- const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
- apiWrapper(req, res, handler, { withAuth: true })
- export default wrapper
- const requestBodySchema = z.object({
- messages: z.array(z.any()),
- projectRef: z.string(),
- connectionString: z.string(),
- schema: z.string().optional(),
- table: z.string().optional(),
- chatId: z.string().optional(),
- chatName: z.string().optional(),
- orgSlug: z.string().optional(),
- model: z.string().optional(),
- })
- async function handlePost(req: NextApiRequest, res: NextApiResponse, claims?: JwtPayload) {
- const authorization = req.headers.authorization
- const accessToken = authorization?.replace('Bearer ', '')
- if (IS_PLATFORM && !accessToken) {
- return res.status(401).json({ error: 'Authorization token is required' })
- }
- const userId = claims?.sub
- const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
- const { data, error: parseError } = requestBodySchema.safeParse(body)
- if (parseError) {
- return res.status(400).json({ error: 'Invalid request body', issues: parseError.issues })
- }
- const {
- messages: rawMessages,
- projectRef,
- connectionString,
- orgSlug,
- chatId,
- chatName,
- model: rawRequestedModel,
- } = data
- const requestedModel: AssistantModelId | undefined =
- rawRequestedModel && isKnownAssistantModelId(rawRequestedModel) ? rawRequestedModel : undefined
- const messagesValidation = await safeValidateUIMessages({
- messages: rawMessages,
- })
- if (!messagesValidation.success) {
- return res.status(400).json({
- error: 'Invalid request body',
- message: messagesValidation.error.message,
- })
- }
- const messages = messagesValidation.data
- let aiOptInLevel: AiOptInLevel = 'disabled'
- let hasAccessToAdvanceModel = false
- let orgHasHipaaAddon: boolean | undefined
- let projectIsSensitive: boolean | undefined
- let projectRegion: string | undefined
- let orgId: number | undefined
- let planId: string | undefined
- if (!IS_PLATFORM) {
- aiOptInLevel = 'schema'
- hasAccessToAdvanceModel = true
- }
- if (IS_PLATFORM && orgSlug && authorization && projectRef) {
- try {
- const [orgDetails, projectDetails] = await Promise.all([
- getOrgAIDetails({ orgSlug, authorization }),
- getProjectAIDetails({ projectRef, authorization }),
- ])
- aiOptInLevel = orgDetails.aiOptInLevel
- hasAccessToAdvanceModel = orgDetails.hasAccessToAdvanceModel
- orgHasHipaaAddon = orgDetails.hasHipaaAddon
- orgId = orgDetails.orgId
- planId = orgDetails.planId
- projectIsSensitive = projectDetails.isSensitive
- projectRegion = projectDetails.region
- } catch (error) {
- return res.status(400).json({
- error: 'There was an error fetching your organization details',
- })
- }
- }
- const envThrottled = process.env.IS_THROTTLED !== 'false'
- let effectiveModel: AssistantModelId = requestedModel ?? DEFAULT_ASSISTANT_ADVANCE_MODEL_ID
- if (!hasAccessToAdvanceModel || (envThrottled && !isAssistantBaseModelId(effectiveModel))) {
- effectiveModel = DEFAULT_ASSISTANT_BASE_MODEL_ID
- }
- const {
- modelParams,
- error: modelError,
- systemProviderOptions,
- } = await getModel({
- provider: 'openai',
- modelEntry: getAssistantModelEntry(effectiveModel),
- })
- if (modelError) {
- return res.status(500).json({ error: modelError.message })
- }
- try {
- const abortController = new AbortController()
- req.on('close', () => abortController.abort())
- req.on('aborted', () => abortController.abort())
- const tools = await getTools({
- projectRef,
- connectionString,
- authorization,
- aiOptInLevel,
- accessToken,
- baseUrl: getURL(),
- })
- // Get a list of all schemas to add to context
- const getSchemas = async (): Promise<string> => {
- const pgMetaSchemasList = pgMeta.schemas.list()
- type Schemas = z.infer<(typeof pgMetaSchemasList)['zod']>
- const { result: schemas } = await executeSql<Schemas>(
- {
- projectRef,
- connectionString,
- sql: pgMetaSchemasList.sql,
- },
- undefined,
- {
- 'Content-Type': 'application/json',
- ...(authorization && { Authorization: authorization }),
- },
- IS_PLATFORM ? undefined : executeQuery
- )
- return schemas?.length > 0
- ? `The available database schema names are: ${JSON.stringify(schemas)}`
- : "You don't have access to any schemas."
- }
- const result = await generateAssistantResponse({
- messages,
- ...modelParams,
- tools,
- aiOptInLevel,
- getSchemas: aiOptInLevel !== 'disabled' ? getSchemas : undefined,
- projectRef,
- chatId,
- chatName,
- allowTracing: isTracingAllowed({
- orgHasHipaaAddon,
- projectIsSensitive,
- projectRegion,
- }),
- userId,
- orgId,
- planId,
- requestedModel,
- systemProviderOptions,
- abortSignal: abortController.signal,
- onSpanCreated: (spanId) => {
- res.setHeader('x-braintrust-span-id', spanId)
- },
- })
- result.pipeUIMessageStreamToResponse(res, {
- sendReasoning: true,
- headers: { 'Content-Encoding': 'none' },
- onError: (error) => {
- console.error('Assistant stream error:', error)
- if (error == null) {
- return 'unknown error'
- }
- if (typeof error === 'string') {
- return error
- }
- if (error instanceof Error) {
- return error.message
- }
- return JSON.stringify(error)
- },
- })
- } catch (error) {
- console.error('Error in handlePost:', error)
- if (error instanceof Error) {
- return res.status(500).json({ message: error.message })
- }
- return res.status(500).json({ message: 'An unexpected error occurred.' })
- }
- }
|