diff --git a/apps/database-new/.env.local.example b/apps/database-new/.env.local.example index 70bad2e5610..eaad2b0e239 100644 --- a/apps/database-new/.env.local.example +++ b/apps/database-new/.env.local.example @@ -3,3 +3,5 @@ NEXT_PUBLIC_SUPABASE_URL=your-project-url NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key OPENAI_API_KEY=your-openai-api-key +OPENAI_BASE_URL=your-openai-compatible-base-url +OPENAI_MODEL=your-language-model diff --git a/apps/database-new/app/[thread_id]/[message_id]/getAssistantMessage.ts b/apps/database-new/app/[thread_id]/[message_id]/getAssistantMessage.ts index 5d5de4d165a..b5acb6e01f9 100644 --- a/apps/database-new/app/[thread_id]/[message_id]/getAssistantMessage.ts +++ b/apps/database-new/app/[thread_id]/[message_id]/getAssistantMessage.ts @@ -1,17 +1,12 @@ import { stripIndent } from 'common-tags' -import { cookies } from 'next/headers' import OpenAI from 'openai' // import is weird, what's up with this? import { createClient } from '@/lib/supabase/server' +import { createOpenAiClient } from 'ai-commands' import { ContextLengthError } from '../../../../../packages/ai-commands/src/errors' import { getMessages } from './getMessages' -// Create an OpenAI API client (that's edge friendly!) -const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, -}) - // Set the runtime to edge for best performance export const runtime = 'edge' @@ -26,6 +21,12 @@ export type AiAssistantMessage = { * it. */ export const getAssistantResponse = async (threadId: string, messageId: string) => { + const { openai, model, error: openAiError } = createOpenAiClient() + + if (openAiError) { + throw openAiError + } + const supabase = createClient() const { data: message, error } = await supabase @@ -91,7 +92,7 @@ export const getAssistantResponse = async (threadId: string, messageId: string) try { const response = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo-16k-0613', + model, messages: initMessages, max_tokens: 1024, temperature: 0, diff --git a/apps/database-new/package.json b/apps/database-new/package.json index d7ed811868e..5b37491c5ae 100644 --- a/apps/database-new/package.json +++ b/apps/database-new/package.json @@ -16,6 +16,7 @@ "@supabase/ssr": "^0.1.0", "@tanstack/react-query": "^5.13.4", "ai": "^2.2.31", + "ai-commands": "*", "common": "*", "config": "*", "dayjs": "^1.11.10", @@ -42,4 +43,4 @@ "postcss": "^8.4.31", "typescript": "^5.4.3" } -} +} \ No newline at end of file diff --git a/apps/studio/.env b/apps/studio/.env index 4de7d26d76e..8dd533d0f4a 100644 --- a/apps/studio/.env +++ b/apps/studio/.env @@ -6,7 +6,6 @@ DEFAULT_PROJECT_NAME=Default Project DASHBOARD_USERNAME=supabase DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated - SUPABASE_URL=http://localhost:8000 SUPABASE_PUBLIC_URL=http://localhost:8000 SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE @@ -24,4 +23,11 @@ NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzUwOTQ4MzUsImV4cCI6MTk5MDY3MDgzNX0.0PMlOxtKL4O9GGZuAP_Xl4f-Tut1qOnW4bNEmAtoB8w NEXT_PUBLIC_SUPABASE_URL=https://xguihxuzqibwxjnimxev.supabase.co -DOCKER_SOCKET_LOCATION=/var/run/docker.sock \ No newline at end of file +OPENAI_API_KEY="your-openai-api-key" +OPENAI_BASE_URL="your-openai-compatible-base-url" +OPENAI_MODEL="your-language-model" +OPENAI_EMBEDDING_API_KEY="your-openai-api-key" +OPENAI_EMBEDDING_BASE_URL="your-openai-compatible-base-url" +OPENAI_EMBEDDING_MODEL="your-embedding-model" + +DOCKER_SOCKET_LOCATION=/var/run/docker.sock diff --git a/apps/studio/pages/api/ai/sql/debug.ts b/apps/studio/pages/api/ai/sql/debug.ts index 965bf49f142..06fb59dd62d 100644 --- a/apps/studio/pages/api/ai/sql/debug.ts +++ b/apps/studio/pages/api/ai/sql/debug.ts @@ -1,18 +1,9 @@ import { ContextLengthError, EmptySqlError, debugSql } from 'ai-commands' +import { createOpenAiClient } from 'ai-commands/src/openai' import apiWrapper from 'lib/api/apiWrapper' import { NextApiRequest, NextApiResponse } from 'next' -import { OpenAI } from 'openai' - -const openAiKey = process.env.OPENAI_KEY -const openai = new OpenAI({ apiKey: openAiKey }) async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!openAiKey) { - return res.status(500).json({ - error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', - }) - } - const { method } = req switch (method) { @@ -25,12 +16,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } export async function handlePost(req: NextApiRequest, res: NextApiResponse) { + const { openai, model, error } = createOpenAiClient() + + if (error) { + return res.status(500).json({ error: error.message }) + } + const { body: { errorMessage, sql, entityDefinitions }, } = req try { - const result = await debugSql(openai, errorMessage, sql, entityDefinitions) + const result = await debugSql(openai, model, errorMessage, sql, entityDefinitions) return res.json(result) } catch (error) { if (error instanceof Error) { diff --git a/apps/studio/pages/api/ai/sql/edit.ts b/apps/studio/pages/api/ai/sql/edit.ts index 3e564c19185..8acc8df2019 100644 --- a/apps/studio/pages/api/ai/sql/edit.ts +++ b/apps/studio/pages/api/ai/sql/edit.ts @@ -1,18 +1,9 @@ import { ContextLengthError, EmptySqlError, editSql } from 'ai-commands' +import { createOpenAiClient } from 'ai-commands/src/openai' import apiWrapper from 'lib/api/apiWrapper' import { NextApiRequest, NextApiResponse } from 'next' -import { OpenAI } from 'openai' - -const openAiKey = process.env.OPENAI_KEY -const openai = new OpenAI({ apiKey: openAiKey }) async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!openAiKey) { - return res.status(500).json({ - error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', - }) - } - const { method } = req switch (method) { @@ -25,12 +16,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } export async function handlePost(req: NextApiRequest, res: NextApiResponse) { + const { openai, model, error } = createOpenAiClient() + + if (error) { + return res.status(500).json({ error: error.message }) + } + const { body: { prompt, sql, entityDefinitions }, } = req try { - const result = await editSql(openai, prompt, sql, entityDefinitions) + const result = await editSql(openai, model, prompt, sql, entityDefinitions) return res.json(result) } catch (error) { if (error instanceof Error) { diff --git a/apps/studio/pages/api/ai/sql/generate-v2.ts b/apps/studio/pages/api/ai/sql/generate-v2.ts index 144f01a93bd..27b4823623e 100644 --- a/apps/studio/pages/api/ai/sql/generate-v2.ts +++ b/apps/studio/pages/api/ai/sql/generate-v2.ts @@ -1,25 +1,11 @@ import { StreamingTextResponse } from 'ai' import { generateV2 } from 'ai-commands/edge' +import { createOpenAiClient } from 'ai-commands/src/openai' import { NextRequest } from 'next/server' -import OpenAI from 'openai' export const runtime = 'edge' -const openAiKey = process.env.OPENAI_KEY - export default async function handler(req: NextRequest) { - if (!openAiKey) { - return new Response( - JSON.stringify({ - error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - const { method } = req switch (method) { @@ -37,7 +23,19 @@ export default async function handler(req: NextRequest) { } async function handlePost(request: NextRequest) { - const openai = new OpenAI({ apiKey: openAiKey }) + const { openai, model, error } = createOpenAiClient() + + if (error) { + return new Response( + JSON.stringify({ + error: error.message, + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } const body = await (request.json() as Promise<{ messages: { content: string; role: 'user' | 'assistant' }[] @@ -48,7 +46,7 @@ async function handlePost(request: NextRequest) { const { messages, existingSql, entityDefinitions } = body try { - const stream = await generateV2(openai, messages, existingSql, entityDefinitions) + const stream = await generateV2(openai, model, messages, existingSql, entityDefinitions) return new StreamingTextResponse(stream) } catch (error) { if (error instanceof Error) { diff --git a/apps/studio/pages/api/ai/sql/generate.ts b/apps/studio/pages/api/ai/sql/generate.ts index 1dc73696ac3..86848aed248 100644 --- a/apps/studio/pages/api/ai/sql/generate.ts +++ b/apps/studio/pages/api/ai/sql/generate.ts @@ -1,18 +1,9 @@ import { ContextLengthError, EmptySqlError, generateSql } from 'ai-commands' +import { createOpenAiClient } from 'ai-commands/src/openai' import apiWrapper from 'lib/api/apiWrapper' import { NextApiRequest, NextApiResponse } from 'next' -import { OpenAI } from 'openai' - -const openAiKey = process.env.OPENAI_KEY -const openai = new OpenAI({ apiKey: openAiKey }) async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!openAiKey) { - return res.status(500).json({ - error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', - }) - } - const { method } = req switch (method) { @@ -25,12 +16,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } export async function handlePost(req: NextApiRequest, res: NextApiResponse) { + const { openai, model, error } = createOpenAiClient() + + if (error) { + return res.status(500).json({ error: error.message }) + } + const { body: { prompt, entityDefinitions }, } = req try { - const result = await generateSql(openai, prompt, entityDefinitions) + const result = await generateSql(openai, model, prompt, entityDefinitions) return res.json(result) } catch (error) { if (error instanceof Error) { diff --git a/apps/studio/pages/api/ai/sql/suggest.ts b/apps/studio/pages/api/ai/sql/suggest.ts index 3f60da2f8ff..757ed6bf7f3 100644 --- a/apps/studio/pages/api/ai/sql/suggest.ts +++ b/apps/studio/pages/api/ai/sql/suggest.ts @@ -1,25 +1,11 @@ import { StreamingTextResponse } from 'ai' import { chatRlsPolicy } from 'ai-commands/edge' +import { createOpenAiClient } from 'ai-commands/src/openai' import { NextRequest } from 'next/server' -import OpenAI from 'openai' export const runtime = 'edge' -const openAiKey = process.env.OPENAI_KEY - export default async function handler(req: NextRequest) { - if (!openAiKey) { - return new Response( - JSON.stringify({ - error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - const { method } = req switch (method) { @@ -37,7 +23,19 @@ export default async function handler(req: NextRequest) { } async function handlePost(request: NextRequest) { - const openai = new OpenAI({ apiKey: openAiKey }) + const { openai, model, error } = createOpenAiClient() + + if (error) { + return new Response( + JSON.stringify({ + error: error.message, + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } const body = await (request.json() as Promise<{ messages: { content: string; role: 'user' | 'assistant' }[] @@ -48,7 +46,7 @@ async function handlePost(request: NextRequest) { const { messages, entityDefinitions, policyDefinition } = body try { - const stream = await chatRlsPolicy(openai, messages, entityDefinitions, policyDefinition) + const stream = await chatRlsPolicy(openai, model, messages, entityDefinitions, policyDefinition) return new StreamingTextResponse(stream) } catch (error) { console.error(error) diff --git a/apps/studio/pages/api/ai/sql/title.ts b/apps/studio/pages/api/ai/sql/title.ts index c5d1477faf9..bd153e35ca4 100644 --- a/apps/studio/pages/api/ai/sql/title.ts +++ b/apps/studio/pages/api/ai/sql/title.ts @@ -1,18 +1,9 @@ import { ContextLengthError, titleSql } from 'ai-commands' +import { createOpenAiClient } from 'ai-commands/src/openai' import apiWrapper from 'lib/api/apiWrapper' import { NextApiRequest, NextApiResponse } from 'next' -import { OpenAI } from 'openai' - -const openAiKey = process.env.OPENAI_KEY -const openai = new OpenAI({ apiKey: openAiKey }) async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!openAiKey) { - return res.status(500).json({ - error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', - }) - } - const { method } = req switch (method) { @@ -25,12 +16,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } export async function handlePost(req: NextApiRequest, res: NextApiResponse) { + const { openai, model, error } = createOpenAiClient() + + if (error) { + return res.status(500).json({ error: error.message }) + } + const { body: { sql }, } = req try { - const result = await titleSql(openai, sql) + const result = await titleSql(openai, model, sql) return res.json(result) } catch (error) { if (error instanceof Error) { diff --git a/package-lock.json b/package-lock.json index d249a3026b2..69dca12ca82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@supabase/ssr": "^0.1.0", "@tanstack/react-query": "^5.13.4", "ai": "^2.2.31", + "ai-commands": "*", "common": "*", "config": "*", "dayjs": "^1.11.10", @@ -6983,6 +6984,14 @@ "react-hook-form": "^7.0.0" } }, + "node_modules/@huggingface/jinja": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz", + "integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==", + "engines": { + "node": ">=18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -8646,6 +8655,60 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@radix-ui/colors": { "version": "0.1.9", "license": "MIT" @@ -17012,6 +17075,11 @@ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/markdown-table": { "version": "3.0.0", "dev": true, @@ -18041,6 +18109,19 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xenova/transformers": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.1.tgz", + "integrity": "sha512-zo702tQAFZXhzeD2GCYUNUqeqkoueOdiSbQWa4s0q7ZE4z8WBIwIsMMPGobpgdqjQ2u0Qulo08wuqVEUrBXjkQ==", + "dependencies": { + "@huggingface/jinja": "^0.2.2", + "onnxruntime-web": "1.14.0", + "sharp": "^0.32.0" + }, + "optionalDependencies": { + "onnxruntime-node": "1.14.0" + } + }, "node_modules/@xobotyi/scrollbar-width": { "version": "1.9.5", "license": "MIT" @@ -18898,8 +18979,7 @@ "node_modules/b4a": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", - "dev": true + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", @@ -19345,14 +19425,12 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.0.tgz", "integrity": "sha512-Yyyqff4PIFfSuthCZqLlPISTWHmnQxoPuAvkmgzsJEmG3CesdIv6Xweayl0JkCZJSB2yYIdJyEz97tpxNhgjbg==", - "dev": true, "optional": true }, "node_modules/bare-fs": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.2.0.tgz", "integrity": "sha512-+VhW202E9eTVGkX7p+TNXtZC4RTzj9JfJW7PtfIbZ7mIQ/QT9uOafQTx7lx2n9ERmWsXvLHF4hStAFn4gl2mQw==", - "dev": true, "optional": true, "dependencies": { "bare-events": "^2.0.0", @@ -19365,14 +19443,12 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.0.tgz", "integrity": "sha512-hD0rOPfYWOMpVirTACt4/nK8mC55La12K5fY1ij8HAdfQakD62M+H4o4tpfKzVGLgRDTuk3vjA4GqGXXCeFbag==", - "dev": true, "optional": true }, "node_modules/bare-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.0.tgz", "integrity": "sha512-DIIg7ts8bdRKwJRJrUMy/PICEaQZaPGZ26lsSx9MJSwIhSrcdHn7/C8W+XmnG/rKi6BaRcz+JO00CjZteybDtw==", - "dev": true, "optional": true, "dependencies": { "bare-os": "^2.1.0" @@ -22207,7 +22283,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -22280,7 +22355,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, "engines": { "node": ">=4.0.0" } @@ -24336,7 +24410,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, "engines": { "node": ">=6" } @@ -24533,8 +24606,7 @@ "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -24889,6 +24961,11 @@ "node": ">=12.0.0" } }, + "node_modules/flatbuffers": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", + "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==" + }, "node_modules/flatted": { "version": "3.2.9", "license": "ISC" @@ -25502,8 +25579,7 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "node_modules/github-slugger": { "version": "2.0.0", @@ -25683,6 +25759,11 @@ "node": ">=6.0" } }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==" + }, "node_modules/gulp-header": { "version": "1.8.12", "license": "MIT", @@ -27100,8 +27181,7 @@ "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/ini-simple-parser": { "version": "1.0.0", @@ -30404,6 +30484,11 @@ "node": ">=8" } }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/longest-streak": { "version": "3.1.0", "license": "MIT", @@ -34588,7 +34673,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -35015,8 +35099,7 @@ "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -35197,7 +35280,6 @@ "version": "3.56.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz", "integrity": "sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==", - "dev": true, "dependencies": { "semver": "^7.3.5" }, @@ -35209,7 +35291,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -35221,7 +35302,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -35235,8 +35315,7 @@ "node_modules/node-abi/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/node-abort-controller": { "version": "3.1.1", @@ -36364,6 +36443,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnx-proto": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz", + "integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==", + "dependencies": { + "protobufjs": "^6.8.8" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz", + "integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==" + }, + "node_modules/onnxruntime-node": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz", + "integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==", + "optional": true, + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "onnxruntime-common": "~1.14.0" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz", + "integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==", + "dependencies": { + "flatbuffers": "^1.12.0", + "guid-typescript": "^1.0.9", + "long": "^4.0.0", + "onnx-proto": "^4.0.4", + "onnxruntime-common": "~1.14.0", + "platform": "^1.3.6" + } + }, "node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -37123,6 +37242,11 @@ "pathe": "^1.1.0" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" + }, "node_modules/playwright": { "version": "1.41.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", @@ -37560,7 +37684,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dev": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -37852,6 +37975,31 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "dev": true, @@ -38132,8 +38280,7 @@ "node_modules/queue-tick": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" }, "node_modules/raf-schd": { "version": "4.0.3", @@ -38246,7 +38393,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -38261,7 +38407,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -41423,7 +41568,6 @@ "version": "0.32.6", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", - "dev": true, "hasInstallScript": true, "dependencies": { "color": "^4.2.3", @@ -41446,7 +41590,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -41457,14 +41600,12 @@ "node_modules/sharp/node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" }, "node_modules/sharp/node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -41479,7 +41620,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", - "dev": true, "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" @@ -41493,7 +41633,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", @@ -41503,8 +41642,7 @@ "node_modules/sharp/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -41619,7 +41757,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, "funding": [ { "type": "github", @@ -41639,7 +41776,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, "funding": [ { "type": "github", @@ -42301,7 +42437,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", - "dev": true, "dependencies": { "fast-fifo": "^1.1.0", "queue-tick": "^1.0.1" @@ -44445,7 +44580,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -47343,6 +47477,7 @@ "@gregnr/libpg-query": "^13.4.0-dev.12", "@serafin/schema-builder": "^0.18.5", "@supabase/supabase-js": "*", + "@xenova/transformers": "^2.17.1", "ai": "^2.2.29", "common-tags": "^1.8.2", "config": "*", diff --git a/packages/ai-commands/index.ts b/packages/ai-commands/index.ts index dc525721fcc..5aee37c5b17 100644 --- a/packages/ai-commands/index.ts +++ b/packages/ai-commands/index.ts @@ -1,2 +1,4 @@ export * from './src/errors' +export * from './src/openai' export * from './src/sql' +export * from './src/tokenizer' diff --git a/packages/ai-commands/jest.config.js b/packages/ai-commands/jest.config.js index b970fa8370a..8b619eb8803 100644 --- a/packages/ai-commands/jest.config.js +++ b/packages/ai-commands/jest.config.js @@ -10,6 +10,6 @@ module.exports = { setupFilesAfterEnv: ['./test/extensions.ts'], testTimeout: 15000, transformIgnorePatterns: [ - 'node_modules/(?!(mdast-.*|micromark|micromark-.*|unist-.*|decode-named-character-reference|character-entities)/)', + 'node_modules/(?!(mdast-.*|micromark|micromark-.*|unist-.*|decode-named-character-reference|character-entities|@xenova/transformers)/)', ], } diff --git a/packages/ai-commands/package.json b/packages/ai-commands/package.json index fe02d5959bc..1524bd0d865 100644 --- a/packages/ai-commands/package.json +++ b/packages/ai-commands/package.json @@ -12,6 +12,7 @@ "@gregnr/libpg-query": "^13.4.0-dev.12", "@serafin/schema-builder": "^0.18.5", "@supabase/supabase-js": "*", + "@xenova/transformers": "^2.17.1", "ai": "^2.2.29", "common-tags": "^1.8.2", "config": "*", diff --git a/packages/ai-commands/src/__snapshots__/sql.test.ts.snap b/packages/ai-commands/src/__snapshots__/sql.test.ts.snap index ed775186dac..48eed7c9f8a 100644 --- a/packages/ai-commands/src/__snapshots__/sql.test.ts.snap +++ b/packages/ai-commands/src/__snapshots__/sql.test.ts.snap @@ -32,9 +32,9 @@ exports[`edit add length constraint 1`] = ` exports[`generate single table with specified columns 1`] = ` "create table employees ( id bigint primary key generated always as identity, - name text, - email text, - position text + name text not null, + email text unique not null, + position text not null );" `; @@ -46,3 +46,25 @@ exports[`rls chat select policy using table definition 1`] = ` "usingExpression": "user_id = auth.uid ()", } `; + +exports[`sql chat single table with specified columns 1`] = ` +"create table employees ( + id bigint primary key generated always as identity, + name text not null, + email text unique not null, + position text not null, + created_at timestamp with time zone default current_timestamp, + updated_at timestamp with time zone default current_timestamp +);" +`; + +exports[`sql chat table referencing users creates fk to auth.users 1`] = ` +"create table todos ( + id bigint primary key generated always as identity, + title text not null, + description text, + created_at timestamp with time zone default current_timestamp, + author_id uuid references auth.users (id), + completed boolean default false +);" +`; diff --git a/packages/ai-commands/src/openai.ts b/packages/ai-commands/src/openai.ts new file mode 100644 index 00000000000..443a80184ea --- /dev/null +++ b/packages/ai-commands/src/openai.ts @@ -0,0 +1,129 @@ +import OpenAI from 'openai' + +export const supportedModels = [ + 'llama3', + 'llama3-70b-8192', + 'llama3-8b-8192', + 'gpt-4-turbo', + 'gpt-4-turbo-preview', + 'gpt-4-turbo-2024-04-09', + 'gpt-4-0125-preview', + 'gpt-4-1106-preview', + 'gpt-4', + 'gpt-4-0613', + 'gpt-4-0314', + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-0125', + 'gpt-3.5-turbo-1106', +] + +export const supportedEmbeddingModels = ['text-embedding-ada-002'] + +/** + * Instantiates OpenAI client while checking and validating + * environment variables. + * + * Allows you to use your own OpenAI-compatible provider. + * + * Uses the following env vars: + * - OPENAI_API_KEY (backwards compatible with previous OPENAI_KEY) + * - OPENAI_BASE_URL + * - OPENAI_MODEL + */ +export function createOpenAiClient() { + const apiKey = process.env.OPENAI_API_KEY ?? process.env.OPENAI_KEY + const baseURL = process.env.OPENAI_BASE_URL + const model = process.env.OPENAI_MODEL + + if (!apiKey) { + return { + error: new Error( + 'No OPENAI_API_KEY set. Create this environment variable to use AI features.' + ), + } + } + + if (!baseURL) { + return { + error: new Error( + 'No OPENAI_BASE_URL set. Create this environment variable to use AI features.' + ), + } + } + + if (!model) { + return { + error: new Error('No OPENAI_MODEL set. Create this environment variable to use AI features.'), + } + } + + if (!supportedModels.includes(model)) { + return { + error: new Error( + `The following models are supported for OPENAI_MODEL: ${JSON.stringify(supportedModels)}` + ), + } + } + + const openai = new OpenAI({ apiKey, baseURL }) + + return { openai, model } +} + +/** + * Instantiates OpenAI client for embedding models while checking and validating + * environment variables. + * + * Allows you to use separate OpenAI-compatible providers for embedding models vs. language models. + * + * Uses the following env vars: + * - OPENAI_EMBEDDING_API_KEY + * - OPENAI_EMBEDDING_BASE_URL + * - OPENAI_EMBEDDING_MODEL + * + * Falls back to these default env vars if the above vars don't exist: + * - OPENAI_API_KEY + * - OPENAI_BASE_URL + */ +export function createOpenAiEmbeddingClient() { + const apiKey = + process.env.OPENAI_EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY ?? process.env.OPENAI_KEY + const baseURL = process.env.OPENAI_EMBEDDING_BASE_URL ?? process.env.OPENAI_BASE_URL + const model = process.env.OPENAI_EMBEDDING_MODEL + + if (!apiKey) { + return { + error: new Error( + 'No OPENAI_EMBEDDING_API_KEY set. Create this environment variable to use AI embedding features.' + ), + } + } + + if (!baseURL) { + return { + error: new Error( + 'No OPENAI_EMBEDDING_BASE_URL set. Create this environment variable to use AI embedding features.' + ), + } + } + + if (!model) { + return { + error: new Error( + 'No OPENAI_EMBEDDING_MODEL set. Create this environment variable to use AI embedding features.' + ), + } + } + + if (!supportedEmbeddingModels.includes(model)) { + return { + error: new Error( + `The following models are supported for OPENAI_EMBEDDING_MODEL: ${JSON.stringify(supportedEmbeddingModels)}` + ), + } + } + + const openai = new OpenAI({ apiKey, baseURL }) + + return { openai, model } +} diff --git a/packages/ai-commands/src/sql.edge.ts b/packages/ai-commands/src/sql.edge.ts index fb5164e9bc2..f34d27ae7c6 100644 --- a/packages/ai-commands/src/sql.edge.ts +++ b/packages/ai-commands/src/sql.edge.ts @@ -3,13 +3,8 @@ import { OpenAIStream } from 'ai' import { codeBlock, oneLine, stripIndent } from 'common-tags' import OpenAI from 'openai' -import { ApplicationError, ContextLengthError, UserError } from './errors' -import { getChatRequestTokenCount, getMaxTokenCount, tokenizer } from './tokenizer' - -interface Message { - role: 'user' | 'assistant' - content: string -} +import { ApplicationError, ContextLengthError } from './errors' +import { Message, capMessages, countTokens } from './tokenizer' /** * Responds to a conversation about building an RLS policy. @@ -18,6 +13,7 @@ interface Message { */ export async function chatRlsPolicy( openai: OpenAI, + model: string, messages: Message[], entityDefinitions?: string[], policyDefinition?: string @@ -26,7 +22,7 @@ export async function chatRlsPolicy( { role: 'system', content: stripIndent` - You're an Postgres expert in writing row level security policies. Your purpose is to + You're a Postgres expert in writing row level security policies. Your purpose is to generate a policy with the constraints given by the user. You will be provided a schema on which the policy should be applied. @@ -35,18 +31,18 @@ export async function chatRlsPolicy( - Always use double apostrophe in SQL strings (eg. 'Night''s watch') - You can use only CREATE POLICY or ALTER POLICY queries, no other queries are allowed. - You can add short explanations to your messages. - - The result should be a valid markdown. The SQL code should be wrapped in \`\`\`. + - The result should be a valid markdown. The SQL code should be wrapped in \`\`\` (including sql language tag). - Always use "auth.uid()" instead of "current_user". - You can't use "USING" expression on INSERT policies. - Only use "WITH CHECK" expression on INSERT or UPDATE policies. - - The policy name should be short text explaining the policy, enclosed in double quotes. + - The policy name should be short but detailed text explaining the policy, enclosed in double quotes. - Always put explanations as separate text. Never use inline SQL comments. - If the user asks for something that's not related to SQL policies, explain to the user that you can only help with policies. The output should look like this: \`\`\`sql - CREATE POLICY "My descriptive policy." ON users FOR INSERT USING (user_name = current_user) WITH (true); + CREATE POLICY "My descriptive policy." ON books FOR INSERT USING (author_id = auth.uid()) WITH (true); \`\`\` `, }, @@ -77,7 +73,7 @@ export async function chatRlsPolicy( try { const response = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo-0125', + model, messages: initMessages, max_tokens: 1024, temperature: 0, @@ -101,24 +97,37 @@ export async function chatRlsPolicy( */ export async function generateV2( openai: OpenAI, + model: string, messages: Message[], existingSql?: string, entityDefinitions?: string[] ): Promise> { - const initMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ + const initMessages: Message[] = [ { role: 'system', content: stripIndent` + You are a Supabase Postgres SQL generator. The user will have a conversation with you and your job is to generate SQL + in the Postgres dialect. Be friendly and relatable, complementing the user when appropriate, but don't go overboard. + The generated SQL (must be valid SQL). - For primary keys, always use "id bigint primary key generated always as identity" (not serial) - Prefer creating foreign key references in the create statement + - When creating new tables, always add foreign key references inline (ie. "my_column my_type references ..." on a single line) + - Include all necessary tables and create them in the correct order (eg. book references author, so create author before book) - Prefer 'text' over 'varchar' - Prefer 'timestamp with time zone' over 'date' - Use vector(384) data type for any embedding/vector related query - Always use double apostrophe in SQL strings (eg. 'Night''s watch') - Always use semicolons + - There is a built-in auth.users table with a uuid id column (but it's immutable) + - Prefer referencing auth.users directly via FK instead of creating a new users table + - Only create your own users table if you need to store additional data about the user such as avatar + - If you do create your own users table, prefer to call it "profiles" and add a FK to auth.users. You must also create a trigger function + that automatically creates the profile when there is a new auth.user + - Sprinkle in detailed explanations throughout your answer, digging into "why" each snippet is necessary + - Explanations should live outside of code blocks - Output as markdown - - Always include code snippets if available + - Always include code snippets if available (including sql language tag, ie. \`\`\`sql\n...\n\`\`\`) `, }, ] @@ -148,7 +157,7 @@ export async function generateV2( try { const response = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo-0125', + model, messages: initMessages, max_tokens: 1024, temperature: 0, @@ -167,6 +176,9 @@ export async function generateV2( export async function clippy( openai: OpenAI, + openaiEmbedding: OpenAI, + model: string, + embeddingModel: string, supabaseClient: SupabaseClient, messages: Message[] ) { @@ -188,25 +200,9 @@ export async function clippy( throw new Error("No message with role 'user'") } - // Moderate the content to comply with OpenAI T&C - const moderationResponses = await Promise.all( - contextMessages.map((message) => openai.moderations.create({ input: message.content })) - ) - - for (const moderationResponse of moderationResponses) { - const [results] = moderationResponse.results - - if (results.flagged) { - throw new UserError('Flagged content', { - flagged: true, - categories: results.categories, - }) - } - } - - const embeddingResponse = await openai.embeddings + const embeddingResponse = await openaiEmbedding.embeddings .create({ - model: 'text-embedding-ada-002', + model: embeddingModel, input: userMessage.content.replaceAll('\n', ' '), }) .catch((error: any) => { @@ -235,8 +231,7 @@ export async function clippy( for (let i = 0; i < pageSections.length; i++) { const pageSection = pageSections[i] const content = pageSection.content - const encoded = tokenizer.encode(content) - tokenCount += encoded.length + tokenCount += await countTokens(content, model) if (tokenCount >= 1500) { break @@ -245,7 +240,7 @@ export async function clippy( contextText += `${content.trim()}\n---\n` } - const initMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ + const initMessages: Message[] = [ { role: 'system', content: codeBlock` @@ -307,20 +302,19 @@ export async function clippy( }, ] - const model = 'gpt-3.5-turbo-0301' - const maxCompletionTokenCount = 1024 + const maxCompletionTokens = 1024 - const completionMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = capMessages( + const completionMessages: Message[] = await capMessages( initMessages, contextMessages, - maxCompletionTokenCount, - model + model, + maxCompletionTokens ) const completionOptions = { model, messages: completionMessages, - max_tokens: 1024, + max_tokens: maxCompletionTokens, temperature: 0, stream: true, } @@ -342,32 +336,3 @@ export async function clippy( return response } - -/** - * Remove context messages until the entire request fits - * the max total token count for that model. - * - * Accounts for both message and completion token counts. - */ -function capMessages( - initMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], - contextMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], - maxCompletionTokenCount: number, - model: string -) { - const maxTotalTokenCount = getMaxTokenCount(model) - const cappedContextMessages = [...contextMessages] - let tokenCount = - getChatRequestTokenCount([...initMessages, ...cappedContextMessages], model) + - maxCompletionTokenCount - - // Remove earlier context messages until we fit - while (tokenCount >= maxTotalTokenCount) { - cappedContextMessages.shift() - tokenCount = - getChatRequestTokenCount([...initMessages, ...cappedContextMessages], model) + - maxCompletionTokenCount - } - - return [...initMessages, ...cappedContextMessages] -} diff --git a/packages/ai-commands/src/sql.test.ts b/packages/ai-commands/src/sql.test.ts index 730832d51ce..1306a0e2dc0 100644 --- a/packages/ai-commands/src/sql.test.ts +++ b/packages/ai-commands/src/sql.test.ts @@ -1,17 +1,23 @@ import { describe, expect, test } from '@jest/globals' import { codeBlock } from 'common-tags' -import OpenAI from 'openai' import { collectStream, extractMarkdownSql, formatSql, getPolicyInfo } from '../test/util' +import { createOpenAiClient } from './openai' import { debugSql, editSql, generateSql, titleSql } from './sql' -import { chatRlsPolicy } from './sql.edge' +import { chatRlsPolicy, generateV2 } from './sql.edge' -const openAiKey = process.env.OPENAI_KEY -const openai = new OpenAI({ apiKey: openAiKey }) +const { openai, model, error } = createOpenAiClient() + +if (error) { + throw error +} + +console.info(`Running AI tests against model '${model}'`) describe('generate', () => { test('single table with specified columns', async () => { const { sql, title } = await generateSql( openai, + model, 'create a table to track employees with name, email, and position' ) @@ -24,6 +30,7 @@ describe('edit', () => { test('add length constraint', async () => { const { sql } = await editSql( openai, + model, 'force name to be at least 4 characters', codeBlock` create table employees ( @@ -42,6 +49,7 @@ describe('debug', () => { test('fix order of operations', async () => { const { sql } = await debugSql( openai, + model, 'relation "departments" does not exist', codeBlock` create table employees ( @@ -64,6 +72,7 @@ describe('debug', () => { test('fix typos', async () => { const { sql, solution } = await debugSql( openai, + model, 'syntax error at or near "fromm"', codeBlock` select * fromm employees; @@ -79,6 +88,7 @@ describe('title', () => { test('title matches content', async () => { const { title, description } = await titleSql( openai, + model, codeBlock` create table employees ( id bigint primary key generated always as identity, @@ -103,6 +113,7 @@ describe('rls chat', () => { test('select policy using table definition', async () => { const responseStream = await chatRlsPolicy( openai, + model, [ { role: 'user', @@ -131,3 +142,32 @@ describe('rls chat', () => { ) }) }) + +describe('sql chat', () => { + test('single table with specified columns', async () => { + const responseStream = await generateV2(openai, model, [ + { + role: 'user', + content: 'create a table to track employees with name, email, and position', + }, + ]) + const responseText = await collectStream(responseStream) + const [sql] = extractMarkdownSql(responseText) + + expect(sql).toMatchSnapshot() + }) + + test('table referencing users creates fk to auth.users', async () => { + const responseStream = await generateV2(openai, model, [ + { + role: 'user', + content: 'create a todos table and track author', + }, + ]) + + const responseText = await collectStream(responseStream) + const [sql] = extractMarkdownSql(responseText) + + expect(sql).toMatchSnapshot() + }) +}) diff --git a/packages/ai-commands/src/sql.ts b/packages/ai-commands/src/sql.ts index bd39f633c9a..2043c00ec6d 100644 --- a/packages/ai-commands/src/sql.ts +++ b/packages/ai-commands/src/sql.ts @@ -9,13 +9,6 @@ const generateSqlSchema = SchemaBuilder.emptySchema() .addString('sql', { description: stripIndent` The generated SQL (must be valid SQL). - - For primary keys, always use "id bigint primary key generated always as identity" (not serial) - - Prefer creating foreign key references in the create statement - - Prefer 'text' over 'varchar' - - Prefer 'timestamp with time zone' over 'date' - - Use vector(384) data type for any embedding/vector related query - - Always use double apostrophe in SQL strings (eg. 'Night''s watch') - - Always use semicolons `, }) .addString('title', { @@ -28,16 +21,6 @@ const generateSqlSchema = SchemaBuilder.emptySchema() const editSqlSchema = SchemaBuilder.emptySchema().addString('sql', { description: stripIndent` The modified SQL (must be valid SQL). - - Assume the query hasn't been executed yet - - For primary keys, always use "id bigint primary key generated always as identity" (not serial) - - When creating tables, always add foreign key references inline - - Prefer 'text' over 'varchar' - - Prefer 'timestamp with time zone' over 'date' - - Use vector(384) data type for any embedding/vector related query - - Always use double apostrophe in SQL strings (eg. 'Night''s watch') - - Use real examples when possible - - Add constraints if requested - - Always use semicolons `, }) @@ -83,28 +66,12 @@ const completionFunctions = { }, debugSql: { name: 'debugSql', - description: stripIndent` - Debugs a Postgres SQL error. Returns the fixed SQL and a solution explaining it. - - Create extensions if they are missing (only for valid extensions) - - Suggest creating tables if they are missing - - Include all of the original SQL - - For primary keys, always use "id bigint primary key generated always as identity" (not serial) - - When creating tables, always add foreign key references inline - - Prefer 'text' over 'varchar' - - Prefer 'timestamp with time zone' over 'date' - - Use vector(384) data type for any embedding/vector related query - - Always use double apostrophe in SQL strings (eg. 'Night''s watch') - - Always use semicolons - `, + description: 'Debugs a Postgres SQL error. Returns the fixed SQL and a solution explaining it.', parameters: debugSqlSchema.schema as Record, }, generateTitle: { name: 'generateTitle', - description: stripIndent` - Generates a short title and summarized description for a Postgres SQL snippet. - - The description should describe why this table was created (eg. "Table to track todos") - `, + description: 'Generates a short title and summarized description for a Postgres SQL snippet.', parameters: generateTitleSchema.schema as Record, }, } satisfies Record @@ -114,11 +81,36 @@ const completionFunctions = { * * @returns The generated SQL along with a title for it. */ -export async function generateSql(openai: OpenAI, prompt: string, entityDefinitions?: string[]) { +export async function generateSql( + openai: OpenAI, + model: string, + prompt: string, + entityDefinitions?: string[] +) { const hasEntityDefinitions = entityDefinitions !== undefined && entityDefinitions.length > 0 const completionMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [] + completionMessages.push({ + role: 'system', + content: stripIndent` + You are a Postgres SQL generator. You must generate Postgres SQL based on a natural language prompt + + The generated SQL (must be valid SQL). + - For primary keys, always use "id bigint primary key generated always as identity" (not serial) + - Prefer creating foreign key references in the create statement + - When creating new tables, always add foreign key references inline (ie. "my_column my_type references ..." on a single line) + - Include all necessary tables and create them in the correct order (eg. book references author, so create author before book) + - Prefer 'text' over 'varchar' + - Prefer 'timestamp with time zone' over 'date' + - Use vector(384) data type for any embedding/vector related query + - Always use double apostrophe in SQL strings (eg. 'Night''s watch') + - Always use semicolons + + Response must contain only JSON satisfying the 'tool_calls'. + `, + }) + if (hasEntityDefinitions) { completionMessages.push({ role: 'user', @@ -136,7 +128,7 @@ export async function generateSql(openai: OpenAI, prompt: string, entityDefiniti try { const completionResponse = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo-0125', + model, messages: completionMessages, max_tokens: 1024, temperature: 0, @@ -188,6 +180,7 @@ export async function generateSql(openai: OpenAI, prompt: string, entityDefiniti */ export async function editSql( openai: OpenAI, + model: string, prompt: string, sql: string, entityDefinitions?: string[] @@ -196,6 +189,27 @@ export async function editSql( const completionMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [] + completionMessages.push({ + role: 'system', + content: stripIndent` + You are a Postgres SQL editor. You must edit Postgres SQL based on the user's instructions. + + - Assume the query hasn't been executed yet + - For primary keys, always use "id bigint primary key generated always as identity" (not serial) + - When creating new tables, always add foreign key references inline (ie. "my_column my_type references ..." on a single line) + - Include all necessary tables and create them in the correct order (eg. book references author, so create author before book) + - Prefer 'text' over 'varchar' + - Prefer 'timestamp with time zone' over 'date' + - Use vector(384) data type for any embedding/vector related query + - Always use double apostrophe in SQL strings (eg. 'Night''s watch') + - Use real examples when possible + - Add constraints if requested + - Always use semicolons + + Response must contain only JSON satisfying the 'tool_calls'. + `, + }) + if (hasEntityDefinitions) { completionMessages.push({ role: 'user', @@ -222,7 +236,7 @@ export async function editSql( try { const completionResponse = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo-0125', + model, messages: completionMessages, max_tokens: 2048, temperature: 0, @@ -274,6 +288,7 @@ export async function editSql( */ export async function debugSql( openai: OpenAI, + model: string, errorMessage: string, sql: string, entityDefinitions?: string[] @@ -282,6 +297,25 @@ export async function debugSql( const completionMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [] + completionMessages.push({ + role: 'system', + content: stripIndent` + You are a Postgres SQL debugger. You must fix the user's SQL and a provide solution explaining it. + + - Create extensions if they are missing (only for valid extensions) + - Suggest creating tables if they are missing + - Include all of the original SQL + - For primary keys, always use "id bigint primary key generated always as identity" (not serial) + - When creating tables, always add foreign key references inline + - Prefer 'text' over 'varchar' + - Prefer 'timestamp with time zone' over 'date' + - Use vector(384) data type for any embedding/vector related query + - Always use double apostrophe in SQL strings (eg. 'Night''s watch') + - Always use semicolons + - Do not mix up the user's error with "tool use failed" + `, + }) + if (hasEntityDefinitions) { completionMessages.push({ role: 'user', @@ -311,7 +345,7 @@ export async function debugSql( try { const completionResponse = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo-0125', + model, messages: completionMessages, max_tokens: 2048, temperature: 0, @@ -361,17 +395,29 @@ export async function debugSql( * * @returns A title and description for the SQL snippet. */ -export async function titleSql(openai: OpenAI, sql: string) { - const completionMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ - { - role: 'user', - content: sql, - }, - ] +export async function titleSql(openai: OpenAI, model: string, sql: string) { + const completionMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [] + + completionMessages.push({ + role: 'system', + content: stripIndent` + You are a Postgres SQL title/description generator. + You must generates a short title and summarized description for a Postgres SQL snippet. + + - The title should be short but still address all parts of the code. + - The description should describe why this table was created (eg. "Table to track todos") + while addressing all parts of the code. + `, + }) + + completionMessages.push({ + role: 'user', + content: sql, + }) try { const completionResponse = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo-0125', + model, messages: completionMessages, max_tokens: 1024, temperature: 0, diff --git a/packages/ai-commands/src/tokenizer.test.ts b/packages/ai-commands/src/tokenizer.test.ts new file mode 100644 index 00000000000..6ae5fffa775 --- /dev/null +++ b/packages/ai-commands/src/tokenizer.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from '@jest/globals' +import { Message, tokenizeChat } from './tokenizer' + +const sampleMessages: Message[] = [ + { role: 'system', content: 'You are a helpful Postgres SQL assistant' }, + { role: 'user', content: 'Help me write SQL to get all movies that start with T' }, + { + role: 'assistant', + content: "Sure!\n```sql\nselect * from movies where title like 'T%';\n```", + }, + { role: 'user', content: 'Thanks! What about S?' }, +] + +describe('tokenizer', () => { + test('counts llama3 tokens correctly', async () => { + const tokens = await tokenizeChat(sampleMessages, 'llama3') + expect(tokens.length).toBe(68) + }) + + test('counts gpt-3.5-turbo tokens correctly', async () => { + const tokens = await tokenizeChat(sampleMessages, 'gpt-3.5-turbo') + expect(tokens.length).toBe(66) + }) +}) diff --git a/packages/ai-commands/src/tokenizer.ts b/packages/ai-commands/src/tokenizer.ts index b00be5ab75f..6832b6a7f01 100644 --- a/packages/ai-commands/src/tokenizer.ts +++ b/packages/ai-commands/src/tokenizer.ts @@ -1,88 +1,191 @@ -import { getEncoding } from 'js-tiktoken' -import OpenAI from 'openai' +import { AutoTokenizer, PreTrainedTokenizer } from '@xenova/transformers' -export const tokenizer = getEncoding('cl100k_base') +export interface Message { + role: 'system' | 'user' | 'assistant' + content: string +} -/** - * Count the tokens for multi-message chat completion requests - */ -export function getChatRequestTokenCount( - messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], - model = 'gpt-3.5-turbo-0301' -): number { - const tokensPerRequest = 3 // every reply is primed with <|im_start|>assistant<|im_sep|> - const numTokens = messages.reduce((acc, message) => acc + getMessageTokenCount(message, model), 0) - - return numTokens + tokensPerRequest +function isNumberArray(value: any): value is number[] { + return Array.isArray(value) && typeof value[0] === 'number' } /** - * Count the tokens for a single message within a chat completion request - * - * See "Counting tokens for chat API calls" - * from https://github.com/openai/openai-cookbook/blob/834181d5739740eb8380096dac7056c925578d9a/examples/How_to_count_tokens_with_tiktoken.ipynb + * Gets the Hugging Face model path for the given model. */ -export function getMessageTokenCount( - message: OpenAI.Chat.Completions.ChatCompletionMessageParam, - model = 'gpt-3.5-turbo-0301' -): number { - let tokensPerMessage: number - let tokensPerName: number - +function getModelPath(model: string) { switch (model) { - case 'gpt-3.5-turbo': - console.warn( - 'Warning: gpt-3.5-turbo may change over time. Returning num tokens assuming gpt-3.5-turbo-0301.' - ) - return getMessageTokenCount(message, 'gpt-3.5-turbo-0301') + case 'llama3': + case 'llama3-70b-8192': + case 'llama3-8b-8192': + return 'Xenova/llama-3-tokenizer' + case 'gpt-4-turbo': + case 'gpt-4-turbo-preview': + case 'gpt-4-turbo-2024-04-09': + case 'gpt-4-0125-preview': + case 'gpt-4-1106-preview': + case 'gpt-4-vision-preview': + case 'gpt-4-1106-vision-preview': + case 'gpt-4-32k': + case 'gpt-4-32k-0613': case 'gpt-4': - console.warn('Warning: gpt-4 may change over time. Returning num tokens assuming gpt-4-0314.') - return getMessageTokenCount(message, 'gpt-4-0314') - case 'gpt-3.5-turbo-0301': - tokensPerMessage = 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n - tokensPerName = -1 // if there's a name, the role is omitted - break + case 'gpt-4-0613': case 'gpt-4-0314': - tokensPerMessage = 3 - tokensPerName = 1 - break - default: - throw new Error( - `Unknown model '${model}'. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.` - ) - } - - return Object.entries(message).reduce((acc, [key, value]) => { - acc += tokenizer.encode(value).length - if (key === 'name') { - acc += tokensPerName - } - return acc - }, tokensPerMessage) -} - -/** - * Get the maximum number of tokens for a model's context. - * - * Includes tokens in both message and completion. - */ -export function getMaxTokenCount(model: string): number { - switch (model) { case 'gpt-3.5-turbo': - console.warn( - 'Warning: gpt-3.5-turbo may change over time. Returning max num tokens assuming gpt-3.5-turbo-0301.' - ) - return getMaxTokenCount('gpt-3.5-turbo-0301') - case 'gpt-4': - console.warn( - 'Warning: gpt-4 may change over time. Returning max num tokens assuming gpt-4-0314.' - ) - return getMaxTokenCount('gpt-4-0314') + case 'gpt-3.5-turbo-0125': + case 'gpt-3.5-turbo-1106': + case 'gpt-3.5-turbo-16k': + case 'gpt-3.5-turbo-16k-0613': + case 'gpt-3.5-turbo-instruct': + case 'gpt-3.5-turbo-0613': case 'gpt-3.5-turbo-0301': - return 4097 - case 'gpt-4-0314': - return 4097 + return 'Xenova/gpt-4' default: throw new Error(`Unknown model '${model}'`) } } + +const tokenizers = new Map() + +/** + * Creates a tokenizer for the specified model using + * Transformers.js and config downloaded from Hugging Face. + * + * Caches the tokenizer in memory for subsequent uses. + */ +async function getTokenizer(model: string) { + const cachedTokenizer = tokenizers.get(model) + + if (cachedTokenizer) { + return cachedTokenizer + } + + const modelPath = getModelPath(model) + const tokenizer = await AutoTokenizer.from_pretrained(modelPath) + + tokenizers.set(model, tokenizer) + + return tokenizer +} + +/** + * Tokenizes plain text for the specified model. + */ +export async function tokenize(content: string, model: string) { + const tokenizer = await getTokenizer(model) + + const tokens = tokenizer.encode(content) + + return tokens +} + +/** + * Tokenizes chat messages for the specified model. + * + * Includes delimiter tokens like <|im_start|>, <|im_end|>, etc + */ +export async function tokenizeChat(messages: Message[], model: string) { + const tokenizer = await getTokenizer(model) + + const tokens = tokenizer.apply_chat_template(messages, { + tokenize: true, + return_tensor: false, + add_generation_prompt: true, + }) + + if (!isNumberArray(tokens)) { + throw new Error(`Unexpected tokenizer output. Expected number array.`) + } + + return tokens +} + +/** + * Counts the number of tokens in plain text for the specified model. + */ +export async function countTokens(content: string, model: string) { + const tokens = await tokenize(content, model) + + return tokens.length +} + +/** + * Counts the number of tokens in the chat messages for the specified model. + * + * Accounts for delimiter tokens like <|im_start|>, <|im_end|>, etc + */ +export async function countChatTokens(messages: Message[], model: string) { + const tokens = await tokenizeChat(messages, model) + + return tokens.length +} + +/** + * Get the maximum number of tokens in a model's context window. + * + * Defaults to 4096 for unknown models. + */ +export function getContextWindow(model: string): number { + switch (model) { + case 'gpt-4-turbo': + case 'gpt-4-turbo-preview': + case 'gpt-4-turbo-2024-04-09': + case 'gpt-4-0125-preview': + case 'gpt-4-1106-preview': + case 'gpt-4-vision-preview': + case 'gpt-4-1106-vision-preview': + return 128000 + case 'gpt-4-32k': + case 'gpt-4-32k-0613': + return 32768 + case 'gpt-3.5-turbo': + case 'gpt-3.5-turbo-0125': + case 'gpt-3.5-turbo-1106': + case 'gpt-3.5-turbo-16k': + case 'gpt-3.5-turbo-16k-0613': + return 16385 + case 'gpt-4': + case 'gpt-4-0613': + case 'llama3': + case 'llama3-70b-8192': + case 'llama3-8b-8192': + return 8192 + case 'gpt-4-0314': + case 'gpt-3.5-turbo-instruct': + case 'gpt-3.5-turbo-0613': + case 'gpt-3.5-turbo-0301': + default: + return 4096 + } +} + +/** + * Remove conversation messages as needed until the entire + * request fits the context window of the specified model. + * + * @param systemMessages The initial system messages that must always exist + * @param conversationMessages The conversation messages that can be trimmed if necessary + * @param model The model that will be responding to these messages + * @param maxCompletionTokens The number of response tokens to leave room for + * + * @returns The final set of messages that will fit in the context window of the model. + */ +export async function capMessages( + systemMessages: Message[], + conversationMessages: Message[], + model: string, + maxCompletionTokens = 0 +) { + const contextWindow = getContextWindow(model) + const trimmedConversationMessages = [...conversationMessages] + + let tokenCount = await countChatTokens([...systemMessages, ...conversationMessages], model) + + // Remove earlier conversation messages until we fit + while (tokenCount + maxCompletionTokens >= contextWindow) { + trimmedConversationMessages.shift() + + tokenCount = await countChatTokens([...systemMessages, ...conversationMessages], model) + } + + return [...systemMessages, ...trimmedConversationMessages] +} diff --git a/packages/ai-commands/test/extensions.ts b/packages/ai-commands/test/extensions.ts index bcce558937f..939a3f7474a 100644 --- a/packages/ai-commands/test/extensions.ts +++ b/packages/ai-commands/test/extensions.ts @@ -1,14 +1,15 @@ import { expect } from '@jest/globals' import { codeBlock } from 'common-tags' -import OpenAI from 'openai' +import { createOpenAiClient } from '../src/openai' -const openAiKey = process.env.OPENAI_KEY -const openai = new OpenAI({ apiKey: openAiKey }) +const { openai, model, error } = createOpenAiClient() + +if (error) { + throw error +} expect.extend({ async toMatchCriteria(received: string, criteria: string) { - const model = 'gpt-4-1106-preview' - const completionResponse = await openai.chat.completions.create({ model, messages: [ @@ -21,7 +22,9 @@ expect.extend({ - \`{ "pass": true, "reason": "" }\` if 'Received' adheres to the test 'Criteria' - \`{ "pass": false, "reason": "" }\` if 'Received' does not adhere to the test 'Criteria' + Be critical and make sure every part of 'Criteria' is addressed in 'Received'. The "reason" must explain exactly which part of 'Received' did or did not pass the test 'Criteria'. + Be detailed as to why you failed the test. `, }, {