generate-v4.ts 7.2 KB

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