studio-tools.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta'
  2. import { tool } from 'ai'
  3. import { z } from 'zod'
  4. import { deployEdgeFunction } from '@/data/edge-functions/edge-functions-deploy-mutation'
  5. import { executeSql } from '@/data/sql/execute-sql-query'
  6. import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
  7. import {
  8. EDGE_FUNCTION_PROMPT,
  9. PG_BEST_PRACTICES,
  10. REALTIME_PROMPT,
  11. RLS_PROMPT,
  12. } from '@/lib/ai/prompts'
  13. import { NO_DATA_PERMISSIONS } from '@/lib/ai/tools/tool-sanitizer'
  14. import { fixSqlBackslashEscapes } from '@/lib/ai/util'
  15. const KNOWLEDGE = {
  16. pg_best_practices: PG_BEST_PRACTICES,
  17. rls: RLS_PROMPT,
  18. edge_functions: EDGE_FUNCTION_PROMPT,
  19. realtime: REALTIME_PROMPT,
  20. } as const
  21. type KnowledgeName = keyof typeof KNOWLEDGE
  22. export const executeSqlInputSchema = z.object({
  23. // Transform at parse time so the corrected SQL is what gets stored in
  24. // toolCall.input — ensuring evals and logs reflect what actually runs.
  25. sql: z.string().describe('The SQL statement to execute.').transform(fixSqlBackslashEscapes),
  26. label: z.string().describe('A short 2-4 word label for the SQL statement.'),
  27. chartConfig: z
  28. .object({
  29. view: z.enum(['table', 'chart']).describe('How to render the results after execution'),
  30. xAxis: z.string().optional().describe('The column to use for the x-axis of the chart.'),
  31. yAxis: z.string().optional().describe('The column to use for the y-axis of the chart.'),
  32. })
  33. .describe('Chart configuration for rendering the results'),
  34. isWriteQuery: z
  35. .boolean()
  36. .default(false)
  37. .describe(
  38. 'Whether the SQL statement performs a write operation or has side effects. Set true for INSERT/UPDATE/DELETE/DDL and for SELECT statements that call side-effecting functions, such as select cron.schedule(...), cron.unschedule(...), or functions that create, modify, schedule, enqueue, notify, or trigger work.'
  39. ),
  40. })
  41. export const loadKnowledgeInputSchema = z.object({
  42. name: z
  43. .enum(Object.keys(KNOWLEDGE) as [KnowledgeName, ...KnowledgeName[]])
  44. .describe('The knowledge to load'),
  45. })
  46. export type StudioToolsContext = {
  47. projectRef?: string
  48. connectionString?: string
  49. authorization?: string
  50. aiOptInLevel?: AiOptInLevel
  51. }
  52. export const getStudioTools = (ctx: StudioToolsContext = {}) => {
  53. const { projectRef, connectionString, authorization, aiOptInLevel = 'schema' } = ctx
  54. const authHeaders = authorization
  55. ? { 'Content-Type': 'application/json', Authorization: authorization }
  56. : undefined
  57. return {
  58. execute_sql: tool({
  59. description:
  60. 'Asks the user to execute a SQL statement and return the results. Requires user approval before executing.',
  61. inputSchema: executeSqlInputSchema,
  62. needsApproval: true,
  63. execute: async ({ sql }) => {
  64. // The `needsApproval: true` gate on this tool means the user has
  65. // explicitly approved this AI-generated SQL before execute runs —
  66. // that approval is the user gesture that promotes untrusted to safe.
  67. const { result } = await executeSql(
  68. { projectRef, connectionString, sql: acceptUntrustedSql(untrustedSql(sql)) },
  69. undefined,
  70. authHeaders
  71. )
  72. return result
  73. },
  74. toModelOutput: ({ output }) => {
  75. return aiOptInLevel === 'schema_and_log_and_data'
  76. ? { type: 'json', value: output }
  77. : { type: 'text', value: NO_DATA_PERMISSIONS }
  78. },
  79. }),
  80. deploy_edge_function: tool({
  81. description:
  82. 'Asks the user to deploy a Briven Edge Function from provided code. Requires user approval before deploying.',
  83. inputSchema: z.object({
  84. name: z.string().describe('The URL-friendly name/slug of the Edge Function.'),
  85. code: z.string().describe('The TypeScript code for the Edge Function.'),
  86. }),
  87. needsApproval: true,
  88. execute: async ({ name, code }) => {
  89. await deployEdgeFunction({
  90. projectRef: projectRef ?? '',
  91. slug: name,
  92. metadata: {
  93. entrypoint_path: 'index.ts',
  94. name,
  95. verify_jwt: true,
  96. },
  97. files: [{ name: 'index.ts', content: code }],
  98. authorization,
  99. })
  100. return { success: true }
  101. },
  102. }),
  103. rename_chat: tool({
  104. description: `Rename the current chat session when the current chat name doesn't describe the conversation topic.`,
  105. inputSchema: z.object({
  106. newName: z.string().describe('The new name for the chat session. Five words or less.'),
  107. }),
  108. execute: async () => {
  109. return { status: 'Chat request sent to client' }
  110. },
  111. }),
  112. load_knowledge: tool({
  113. description:
  114. 'Load detailed knowledge about a Briven topic before answering questions about it.',
  115. inputSchema: loadKnowledgeInputSchema,
  116. execute: ({ name }) => KNOWLEDGE[name],
  117. }),
  118. }
  119. }