diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 01cbee09063..e7c264fb0d9 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -7,6 +7,7 @@ import { useAppStateSnapshot } from 'state/app-state' import { Badge, Button, Modal, ScrollArea, cn } from 'ui' import { FEATURE_PREVIEWS, useFeaturePreviewContext } from './FeaturePreviewContext' import { useFlag } from 'hooks/ui/useFlag' +import { LOCAL_STORAGE_KEYS } from 'lib/constants' const FeaturePreviewModal = () => { const snap = useAppStateSnapshot() @@ -40,6 +41,13 @@ const FeaturePreviewModal = () => { action: isSelectedFeatureEnabled ? 'disabled' : 'enabled', label: selectedFeatureKey, }) + + if ( + selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT && + isSelectedFeatureEnabled + ) { + snap.setAiAssistantPanel({ open: false }) + } } function handleCloseFeaturePreviewModal() { diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx index 994de7cb037..c66c6f3b77f 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx @@ -94,15 +94,18 @@ export const AiAssistantPanel = ({ // Use chat id because useChat doesn't have a reset function to clear all messages const { messages: chatMessages, - append, isLoading, + append, } = useChat({ id: chatId, api: `${BASE_PATH}/api/ai/sql/generate-v2`, body: { - existingSql: existingSql, + existingSql, entityDefinitions: entityDefinitions, }, + onError: (error) => { + console.log('Boom: ', error) + }, }) const messages = useMemo(() => { const merged = [...chatMessages.map((m) => ({ ...m, isDebug: false }))] diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts index 3c10430f71e..eb510b1988a 100644 --- a/apps/studio/middleware.ts +++ b/apps/studio/middleware.ts @@ -10,6 +10,9 @@ const HOSTED_SUPPORTED_API_URLS = [ '/ai/sql/suggest', '/ai/sql/generate-v2', '/ai/sql/generate-v3', + '/ai/sql/title', + '/ai/sql/debug', + '/ai/sql/cron', '/get-ip-address', ] diff --git a/apps/studio/pages/api/ai/sql/generate-v2.ts b/apps/studio/pages/api/ai/sql/generate-v2.ts index c1d55f7666b..c3e8e8d7fdf 100644 --- a/apps/studio/pages/api/ai/sql/generate-v2.ts +++ b/apps/studio/pages/api/ai/sql/generate-v2.ts @@ -1,6 +1,7 @@ import { StreamingTextResponse } from 'ai' -import { chatSql } from 'ai-commands/edge' +import { chatRlsPolicy, chatSql } from 'ai-commands/edge' import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types' +import { DatabasePoliciesData } from 'data/database-policies/database-policies-query' import { NextRequest } from 'next/server' import OpenAI from 'openai' @@ -8,7 +9,7 @@ export const config = { runtime: 'edge', /* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that overlap with the OpenAI API regions. - + Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories */ @@ -68,17 +69,29 @@ async function handlePost(request: NextRequest) { const openai = new OpenAI({ apiKey: openAiKey }) const body = await (request.json() as Promise<{ + context?: SupportedAssistantEntities messages: { content: string; role: 'user' | 'assistant' }[] existingSql?: string entityDefinitions: string[] - context?: SupportedAssistantEntities + existingPolicies?: DatabasePoliciesData }>) - const { messages, existingSql, entityDefinitions, context } = body + const { messages, existingSql, entityDefinitions, context, existingPolicies } = body try { - const stream = await chatSql(openai, messages, existingSql, entityDefinitions, context) - return new StreamingTextResponse(stream) + if (context === 'rls-policies') { + const stream = await chatRlsPolicy( + openai, + messages, + entityDefinitions, + existingPolicies ?? [], + existingSql + ) + return new StreamingTextResponse(stream) + } else { + const stream = await chatSql(openai, messages, existingSql, entityDefinitions, context) + return new StreamingTextResponse(stream) + } } catch (error) { if (error instanceof Error) { console.error(`AI SQL generation-v2 failed: ${error.message}`) @@ -86,14 +99,9 @@ async function handlePost(request: NextRequest) { console.error(`AI SQL generation-v2 failed: ${error}`) } - return new Response( - JSON.stringify({ - error: 'There was an error processing your request', - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) + return new Response(JSON.stringify({ error: 'There was an error processing your request' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }) } } diff --git a/apps/studio/pages/api/ai/sql/generate-v3.ts b/apps/studio/pages/api/ai/sql/generate-v3.ts index 0a704e5ff5b..117c7902086 100644 --- a/apps/studio/pages/api/ai/sql/generate-v3.ts +++ b/apps/studio/pages/api/ai/sql/generate-v3.ts @@ -6,10 +6,39 @@ import { executeSql } from 'data/sql/execute-sql-query' import { NextApiRequest, NextApiResponse } from 'next' export const maxDuration = 30 - +const openAiKey = process.env.OPENAI_API_KEY const pgMetaSchemasList = pgMeta.schemas.list() export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!openAiKey) { + return new Response( + JSON.stringify({ + error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + return new Response( + JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }), + { + status: 405, + headers: { 'Content-Type': 'application/json', Allow: 'POST' }, + } + ) + } +} + +async function handlePost(req: NextApiRequest, res: NextApiResponse) { const { messages, projectRef, connectionString, includeSchemaMetadata } = req.body if (!projectRef) { diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx index 3cba1b29e97..80dc8d97269 100644 --- a/apps/studio/pages/project/[ref]/auth/policies.tsx +++ b/apps/studio/pages/project/[ref]/auth/policies.tsx @@ -22,6 +22,7 @@ import type { NextPageWithLayout } from 'types' import { Button, Input } from 'ui' import { DocsButton } from 'components/ui/DocsButton' import { useAppStateSnapshot } from 'state/app-state' +import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' /** * Filter tables by table name and policy name @@ -65,6 +66,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => { }>() const { schema = 'public', search: searchString = '' } = params const { project } = useProjectContext() + const isAssistantV2Enabled = useIsDatabaseFunctionsAssistantEnabled() const { setAiAssistantPanel, aiAssistantPanel } = useAppStateSnapshot() const [selectedTable, setSelectedTable] = useState() @@ -150,10 +152,15 @@ const AuthPoliciesPage: NextPageWithLayout = () => { hasTables={tables.length > 0} isLocked={isLocked} onSelectCreatePolicy={(table: string) => { - setAiAssistantPanel({ - open: true, - initialInput: `Help me create an RLS policy for the table ${table} on the ${schema} schema`, - }) + if (isAssistantV2Enabled) { + setAiAssistantPanel({ + open: true, + initialInput: `Help me create an RLS policy for the table ${table} on the ${schema} schema`, + }) + } else { + setSelectedTable(table) + setShowPolicyAiEditor(true) + } }} onSelectEditPolicy={(policy) => { setSelectedPolicyToEdit(policy) diff --git a/package-lock.json b/package-lock.json index 226d5f6f4f4..9f3795ae99f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18552,44 +18552,6 @@ "node": ">=8" } }, - "node_modules/ai": { - "version": "2.2.37", - "resolved": "https://registry.npmjs.org/ai/-/ai-2.2.37.tgz", - "integrity": "sha512-JIYm5N1muGVqBqWnvkt29FmXhESoO5TcDxw74OE41SsM+uIou6NPDDs0XWb/ABcd1gmp6k5zym64KWMPM2xm0A==", - "license": "Apache-2.0", - "dependencies": { - "eventsource-parser": "1.0.0", - "nanoid": "3.3.6", - "solid-swr-store": "0.10.7", - "sswr": "2.0.0", - "swr": "2.2.0", - "swr-store": "0.10.6", - "swrv": "1.0.4" - }, - "engines": { - "node": ">=14.6" - }, - "peerDependencies": { - "react": "^18.2.0", - "solid-js": "^1.7.7", - "svelte": "^3.0.0 || ^4.0.0", - "vue": "^3.3.4" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "solid-js": { - "optional": true - }, - "svelte": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, "node_modules/ai-commands": { "resolved": "packages/ai-commands", "link": true @@ -23205,14 +23167,6 @@ "node": ">=0.8.x" } }, - "node_modules/eventsource-parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz", - "integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==", - "engines": { - "node": ">=14.18" - } - }, "node_modules/execa": { "version": "5.1.1", "dev": true, @@ -39640,6 +39594,7 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.14.1.tgz", "integrity": "sha512-ZlC9y1KVDhZFdEHLYZup1RjKDutyX1tt3ffOauqRbRURa2vRr2NU/bHuVEuNEqR3zE2uCU3WM6LqH6Oinc3tWg==", + "optional": true, "peer": true, "engines": { "node": ">=10" @@ -40083,24 +40038,13 @@ "version": "1.8.6", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.6.tgz", "integrity": "sha512-yiH6ZfBBZ3xj/aU/PBpVKB+8r8WWp100NGF7k/Z0IrK9Y8Lv0jwvFiJY1cHdc6Tj7GqXArKnMBabM0m1k+LzkA==", + "optional": true, "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "^0.14.1" } }, - "node_modules/solid-swr-store": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/solid-swr-store/-/solid-swr-store-0.10.7.tgz", - "integrity": "sha512-A6d68aJmRP471aWqKKPE2tpgOiR5fH4qXQNfKIec+Vap+MGQm3tvXlT8n0I8UgJSlNAsSAUuw2VTviH2h3Vv5g==", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "solid-js": "^1.2", - "swr-store": "^0.10" - } - }, "node_modules/sonner": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", @@ -40256,17 +40200,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/sswr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sswr/-/sswr-2.0.0.tgz", - "integrity": "sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==", - "dependencies": { - "swrev": "^4.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0" - } - }, "node_modules/stack-generator": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", @@ -41302,28 +41235,6 @@ "node": ">= 4.7.0" } }, - "node_modules/swr": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", - "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==", - "dependencies": { - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/swr-store": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/swr-store/-/swr-store-0.10.6.tgz", - "integrity": "sha512-xPjB1hARSiRaNNlUQvWSVrG5SirCjk2TmaUyzzvk69SZQan9hCJqw/5rG9iL7xElHU784GxRPISClq4488/XVw==", - "dependencies": { - "dequal": "^2.0.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/swrev": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz", @@ -45151,7 +45062,7 @@ "dependencies": { "@serafin/schema-builder": "^0.18.5", "@supabase/supabase-js": "*", - "ai": "^2.2.29", + "ai": "^3.4.33", "common-tags": "^1.8.2", "config": "*", "js-tiktoken": "^1.0.10", @@ -45159,6 +45070,7 @@ "openai": "^4.26.1" }, "devDependencies": { + "@ai-sdk/openai": "^0.0.72", "@babel/core": "^7.23.6", "@babel/preset-env": "^7.23.6", "@jest/globals": "^29.7.0", @@ -45177,6 +45089,82 @@ "typescript": "~5.5.0" } }, + "packages/ai-commands/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, + "packages/ai-commands/node_modules/ai": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/ai/-/ai-3.4.33.tgz", + "integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==", + "dependencies": { + "@ai-sdk/provider": "0.0.26", + "@ai-sdk/provider-utils": "1.0.22", + "@ai-sdk/react": "0.0.70", + "@ai-sdk/solid": "0.0.54", + "@ai-sdk/svelte": "0.0.57", + "@ai-sdk/ui-utils": "0.0.50", + "@ai-sdk/vue": "0.0.59", + "@opentelemetry/api": "1.9.0", + "eventsource-parser": "1.1.2", + "json-schema": "^0.4.0", + "jsondiffpatch": "0.6.0", + "secure-json-parse": "^2.7.0", + "zod-to-json-schema": "^3.23.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "openai": "^4.42.0", + "react": "^18 || ^19 || ^19.0.0-rc", + "sswr": "^2.1.0", + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0", + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + }, + "react": { + "optional": true + }, + "sswr": { + "optional": true + }, + "svelte": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "packages/ai-commands/node_modules/ai/node_modules/@ai-sdk/vue": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.59.tgz", + "integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==", + "dependencies": { + "@ai-sdk/provider-utils": "1.0.22", + "@ai-sdk/ui-utils": "0.0.50", + "swrv": "^1.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "vue": "^3.3.4" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, "packages/ai-commands/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -45195,6 +45183,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "packages/ai-commands/node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "engines": { + "node": ">=14.18" + } + }, "packages/ai-commands/node_modules/sql-formatter": { "version": "15.0.2", "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.0.2.tgz", @@ -45209,6 +45205,19 @@ "sql-formatter": "bin/sql-formatter-cli.cjs" } }, + "packages/ai-commands/node_modules/sswr": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz", + "integrity": "sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==", + "optional": true, + "peer": true, + "dependencies": { + "swrev": "^4.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0" + } + }, "packages/api-types": { "version": "0.0.0", "license": "MIT", diff --git a/packages/ai-commands/package.json b/packages/ai-commands/package.json index 49d84317fc5..4a8efe0ad86 100644 --- a/packages/ai-commands/package.json +++ b/packages/ai-commands/package.json @@ -13,7 +13,7 @@ "dependencies": { "@serafin/schema-builder": "^0.18.5", "@supabase/supabase-js": "*", - "ai": "^2.2.29", + "ai": "^3.4.33", "common-tags": "^1.8.2", "config": "*", "js-tiktoken": "^1.0.10", @@ -22,6 +22,7 @@ }, "devDependencies": { "api-types": "*", + "@ai-sdk/openai": "^0.0.72", "@babel/core": "^7.23.6", "@babel/preset-env": "^7.23.6", "@jest/globals": "^29.7.0", diff --git a/packages/ai-commands/src/sql/chat.ts b/packages/ai-commands/src/sql/chat.ts index cc38825a598..426d9f8f617 100644 --- a/packages/ai-commands/src/sql/chat.ts +++ b/packages/ai-commands/src/sql/chat.ts @@ -25,8 +25,6 @@ export async function chatSql( - Always use semicolons - Output as markdown - Always include code snippets if available - - If a code snippet is SQL, the first line of the snippet should always be -- props: {"title": "Query title", "isChart": "true", "xAxis": "columnName", "yAxis": "columnName"} - - Explain what the snippet does in a sentence or two before showing it - Use vector(384) data type for any embedding/vector related query When generating tables, do the following: @@ -45,6 +43,8 @@ export async function chatSql( // is definitely not optimal at all, but just to get an idea started const generateInstructionsBasedOnContext = () => { switch (context) { + // [Joshen] Sorry for the mess, there's duplicate logic here between this and rls.ts - i'm wondering what should be the best practice + // here? Do we (1) Put ALL logic into this file, or (2) we split each context into their own files? Latter might be cleaner? case 'rls-policies': return stripIndent` You're a Supabase Postgres expert in writing row level security policies. Your purpose is to