complete.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import pgMeta, { getEntityDefinitionsSql } from '@supabase/pg-meta'
  2. import { generateText, ModelMessage, stepCountIs, tool } from 'ai'
  3. import { IS_PLATFORM } from 'common'
  4. import { source } from 'common-tags'
  5. import { NextApiRequest, NextApiResponse } from 'next'
  6. import z from 'zod'
  7. import { executeSql } from '@/data/sql/execute-sql-query'
  8. import { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
  9. import { getOrgAIDetails } from '@/lib/ai/ai-details'
  10. import { getModel } from '@/lib/ai/model'
  11. import { DEFAULT_COMPLETION_MODEL } from '@/lib/ai/model.utils'
  12. import {
  13. COMPLETION_PROMPT,
  14. EDGE_FUNCTION_PROMPT,
  15. PG_BEST_PRACTICES,
  16. SECURITY_PROMPT,
  17. SQL_COMPLETION_INSTRUCTIONS,
  18. } from '@/lib/ai/prompts'
  19. import apiWrapper from '@/lib/api/apiWrapper'
  20. import { executeQuery } from '@/lib/api/self-hosted/query'
  21. export const maxDuration = 60
  22. const pgMetaSchemasList = pgMeta.schemas.list()
  23. type Schemas = z.infer<(typeof pgMetaSchemasList)['zod']>
  24. type EntityDefinitionRow = { data: { definitions: Array<{ id: number; sql: string }> } }
  25. type SqlFetchParams = {
  26. projectRef: string
  27. connectionString: string | null | undefined
  28. headers: Record<string, string>
  29. }
  30. type SchemaListResult =
  31. | { error: true }
  32. | { error: false; queriedSchemas: string[]; otherSchemas: string[] }
  33. type SchemaDDLResult = { error: true } | { error: false; sqlDefinitions: string[] }
  34. async function fetchSchemas(
  35. includeSchema: boolean,
  36. { projectRef, connectionString, headers }: SqlFetchParams
  37. ): Promise<{ schemas: Schemas; error: boolean }> {
  38. if (!includeSchema) return { schemas: [], error: false }
  39. try {
  40. const { result } = await executeSql<Schemas>(
  41. { projectRef, connectionString, sql: pgMetaSchemasList.sql },
  42. undefined,
  43. headers,
  44. IS_PLATFORM ? undefined : executeQuery
  45. )
  46. return { schemas: result, error: false }
  47. } catch {
  48. return { schemas: [], error: true }
  49. }
  50. }
  51. async function fetchSchemaDDL(
  52. schemas: string[],
  53. { projectRef, connectionString, headers }: SqlFetchParams
  54. ): Promise<SchemaDDLResult> {
  55. if (schemas.length === 0) return { error: false, sqlDefinitions: [] }
  56. try {
  57. const { result } = await executeSql<EntityDefinitionRow[]>(
  58. { projectRef, connectionString, sql: getEntityDefinitionsSql({ schemas }) },
  59. undefined,
  60. headers,
  61. IS_PLATFORM ? undefined : executeQuery
  62. )
  63. const definitions = result?.[0]?.data?.definitions ?? []
  64. return {
  65. error: false,
  66. sqlDefinitions: definitions.map((d) => d.sql),
  67. }
  68. } catch {
  69. return { error: true }
  70. }
  71. }
  72. function buildDatabaseSchemaSection({
  73. includeSchema,
  74. schemaListResult,
  75. schemaDDLResult,
  76. }: {
  77. includeSchema: boolean
  78. schemaListResult: SchemaListResult
  79. schemaDDLResult: SchemaDDLResult
  80. }): string {
  81. if (!includeSchema) {
  82. return 'Schema context is unavailable — data opt-in is not enabled for this project.'
  83. }
  84. const lines: string[] = []
  85. if (schemaListResult.error) {
  86. lines.push(
  87. "Unable to fetch list of available database schemas. Assume `public` schema, infer others from the user's existing code."
  88. )
  89. } else {
  90. lines.push(`Queried schemas: ${schemaListResult.queriedSchemas.join(', ')}`)
  91. if (schemaListResult.otherSchemas.length > 0)
  92. lines.push(
  93. `Other available schemas (use getSchemaDefinitions tool): ${schemaListResult.otherSchemas.join(', ')}`
  94. )
  95. }
  96. if (schemaDDLResult.error) {
  97. lines.push('Failed to fetch table definitions due to a database error.')
  98. } else {
  99. const defsText =
  100. schemaDDLResult.sqlDefinitions.length > 0
  101. ? schemaDDLResult.sqlDefinitions.join('\n\n')
  102. : 'No table definitions found.'
  103. lines.push(`\n${defsText}`)
  104. }
  105. return lines.join('\n')
  106. }
  107. const requestBodySchema = z.object({
  108. completionMetadata: z.object({
  109. textBeforeCursor: z.string(),
  110. textAfterCursor: z.string(),
  111. prompt: z.string(),
  112. selection: z.string(),
  113. }),
  114. projectRef: z.string(),
  115. connectionString: z.string().nullish(),
  116. orgSlug: z.string().optional(),
  117. language: z.string().optional(),
  118. })
  119. async function handler(req: NextApiRequest, res: NextApiResponse) {
  120. if (req.method !== 'POST') {
  121. return res.status(405).json({ error: `Method ${req.method} Not Allowed` })
  122. }
  123. try {
  124. let body: unknown
  125. try {
  126. body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
  127. } catch {
  128. return res.status(400).json({ error: 'Malformed JSON' })
  129. }
  130. const { data, error: parseError } = requestBodySchema.safeParse(body)
  131. if (parseError) {
  132. return res.status(400).json({ error: 'Invalid request body', issues: parseError.issues })
  133. }
  134. const { completionMetadata, projectRef, connectionString, orgSlug, language } = data
  135. const { textBeforeCursor, textAfterCursor, prompt, selection } = completionMetadata
  136. const authorization = req.headers.authorization
  137. let aiOptInLevel: AiOptInLevel = IS_PLATFORM ? 'disabled' : 'schema'
  138. if (IS_PLATFORM && orgSlug && authorization && projectRef) {
  139. const { aiOptInLevel: orgAIOptInLevel } = await getOrgAIDetails({
  140. orgSlug,
  141. authorization,
  142. })
  143. aiOptInLevel = orgAIOptInLevel
  144. }
  145. const {
  146. modelParams,
  147. error: modelError,
  148. systemProviderOptions,
  149. } = await getModel({
  150. provider: 'openai',
  151. modelEntry: DEFAULT_COMPLETION_MODEL,
  152. })
  153. if (modelError) {
  154. return res.status(500).json({ error: modelError.message })
  155. }
  156. const headers = {
  157. 'Content-Type': 'application/json',
  158. ...(authorization && { Authorization: authorization }),
  159. }
  160. const includeSchema = aiOptInLevel !== 'disabled'
  161. // Fetch schema list first so we can determine which schemas to load DDL for.
  162. // These are best-effort — if they fail, we proceed without DDL context.
  163. const { schemas, error: schemaListError } = await fetchSchemas(includeSchema, {
  164. projectRef,
  165. connectionString,
  166. headers,
  167. })
  168. // Always include public; also eagerly include any non-public schema whose name
  169. // appears as `name.` in the cursor context. Checking against the real schema list
  170. // avoids fetching DDL for table aliases or other false matches. This is robust to
  171. // incomplete SQL (the user may be mid-typing, so a full parser would fail here).
  172. const cursorContext = textBeforeCursor + selection + textAfterCursor
  173. const lowerContext = cursorContext.toLowerCase()
  174. const schemasToFetch = includeSchema
  175. ? [
  176. 'public',
  177. ...schemas
  178. .filter((s) => {
  179. const lower = s.name.toLowerCase()
  180. return (
  181. s.name !== 'public' &&
  182. (lowerContext.includes(lower + '.') || lowerContext.includes(`"${lower}".`))
  183. )
  184. })
  185. .map((s) => s.name),
  186. ]
  187. : []
  188. const schemaDDLResult = await fetchSchemaDDL(schemasToFetch, {
  189. projectRef,
  190. connectionString,
  191. headers,
  192. })
  193. // Reshape the fetched schemas and candidates into a discriminated union over error states
  194. const fetchedSchemaSet = new Set(schemasToFetch)
  195. const schemaListResult: SchemaListResult = schemaListError
  196. ? { error: true }
  197. : {
  198. error: false,
  199. queriedSchemas: schemasToFetch,
  200. otherSchemas: schemas.filter((s) => !fetchedSchemaSet.has(s.name)).map((s) => s.name),
  201. }
  202. // Important: do not use dynamic content in the system prompt or Bedrock will not cache it
  203. const system = source`
  204. ${COMPLETION_PROMPT}
  205. ${language === 'sql' ? SQL_COMPLETION_INSTRUCTIONS : ''}
  206. ${language === 'sql' ? PG_BEST_PRACTICES : EDGE_FUNCTION_PROMPT}
  207. ${SECURITY_PROMPT}
  208. `
  209. const userMessage = source`
  210. ## Database Schema
  211. ${buildDatabaseSchemaSection({ includeSchema, schemaListResult, schemaDDLResult })}
  212. ## Code
  213. \`\`\`${language ?? ''}
  214. ${textBeforeCursor}<selection>${selection}</selection>${textAfterCursor}
  215. \`\`\`
  216. ## Instruction
  217. ${prompt}
  218. `
  219. // Note: these must be of type `CoreMessage` to prevent AI SDK from stripping `providerOptions`
  220. // https://github.com/vercel/ai/blob/81ef2511311e8af34d75e37fc8204a82e775e8c3/packages/ai/core/prompt/standardize-prompt.ts#L83-L88
  221. const coreMessages: ModelMessage[] = [
  222. {
  223. role: 'system',
  224. content: system,
  225. ...(systemProviderOptions && { providerOptions: systemProviderOptions }),
  226. },
  227. {
  228. role: 'user',
  229. content: userMessage,
  230. },
  231. ]
  232. const { text } = await generateText({
  233. ...modelParams,
  234. stopWhen: stepCountIs(5),
  235. messages: coreMessages,
  236. tools:
  237. includeSchema && !schemaListResult.error
  238. ? {
  239. getSchemaDefinitions: tool({
  240. description: 'Get table and column definitions for one or more schemas',
  241. inputSchema: z.object({
  242. schemas: z
  243. .array(z.string())
  244. .describe('The schema names to get the definitions for'),
  245. }),
  246. execute: async ({ schemas: maybeSchemas }) => {
  247. const validSchemas = maybeSchemas.filter((name) =>
  248. schemas.some((s) => s.name === name)
  249. )
  250. const result = await fetchSchemaDDL(validSchemas, {
  251. projectRef,
  252. connectionString,
  253. headers,
  254. })
  255. if (result.error)
  256. return 'Failed to fetch schema definitions due to a database error.'
  257. if (result.sqlDefinitions.length === 0) return 'No table definitions found.'
  258. return result.sqlDefinitions.join('\n\n')
  259. },
  260. }),
  261. }
  262. : undefined,
  263. })
  264. return res.status(200).json(text)
  265. } catch (error) {
  266. console.error('Completion error:', error)
  267. return res.status(500).json({ error: 'Failed to generate completion' })
  268. }
  269. }
  270. const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
  271. apiWrapper(req, res, handler, { withAuth: true })
  272. export default wrapper