diff --git a/apps/studio/lib/ai/generate-assistant-response.ts b/apps/studio/lib/ai/generate-assistant-response.ts new file mode 100644 index 00000000000..6cd7881f8eb --- /dev/null +++ b/apps/studio/lib/ai/generate-assistant-response.ts @@ -0,0 +1,127 @@ +import { + convertToModelMessages, + isToolUIPart, + type LanguageModel, + type ModelMessage, + stepCountIs, + streamText, + type ToolSet, + type UIMessage, +} from 'ai' +import { source } from 'common-tags' + +import type { AiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi' +import { + CHAT_PROMPT, + EDGE_FUNCTION_PROMPT, + GENERAL_PROMPT, + PG_BEST_PRACTICES, + RLS_PROMPT, + REALTIME_PROMPT, + SECURITY_PROMPT, + LIMITATIONS_PROMPT, +} from 'lib/ai/prompts' +import { sanitizeMessagePart } from 'lib/ai/tools/tool-sanitizer' + +export async function generateAssistantResponse({ + messages: rawMessages, + model, + tools, + aiOptInLevel = 'schema', + getSchemas, + projectRef, + chatName, + promptProviderOptions, + providerOptions, + abortSignal, +}: { + messages: UIMessage[] + model: LanguageModel + tools: ToolSet + aiOptInLevel?: AiOptInLevel + getSchemas?: () => Promise + projectRef?: string + chatName?: string + promptProviderOptions?: Record + providerOptions?: Record + abortSignal?: AbortSignal +}) { + // Only returns last 7 messages + // Filters out tools with invalid states + // Filters out tool outputs based on opt-in level using renderingToolOutputParser + const messages = (rawMessages || []).slice(-7).map((msg) => { + if (msg && msg.role === 'assistant' && 'results' in msg) { + const cleanedMsg = { ...msg } + delete cleanedMsg.results + return cleanedMsg + } + if (msg && msg.role === 'assistant' && msg.parts) { + const cleanedParts = msg.parts + .filter((part) => { + if (isToolUIPart(part)) { + const invalidStates = ['input-streaming', 'input-available', 'output-error'] + return !invalidStates.includes(part.state) + } + return true + }) + .map((part) => { + return sanitizeMessagePart(part, aiOptInLevel) + }) + return { ...msg, parts: cleanedParts } + } + return msg + }) + + const schemasString = + aiOptInLevel !== 'disabled' && getSchemas + ? await getSchemas() + : "You don't have access to any schemas." + + // Important: do not use dynamic content in the system prompt or Bedrock will not cache it + const system = source` + ${GENERAL_PROMPT} + ${CHAT_PROMPT} + ${PG_BEST_PRACTICES} + ${RLS_PROMPT} + ${EDGE_FUNCTION_PROMPT} + ${REALTIME_PROMPT} + ${SECURITY_PROMPT} + ${LIMITATIONS_PROMPT} + ` + + // Note: these must be of type `CoreMessage` to prevent AI SDK from stripping `providerOptions` + // https://github.com/vercel/ai/blob/81ef2511311e8af34d75e37fc8204a82e775e8c3/packages/ai/core/prompt/standardize-prompt.ts#L83-L88 + const assistantContent = + projectRef || chatName || schemasString !== "You don't have access to any schemas." + ? `The user's current project is ${projectRef || 'unknown'}. Their available schemas are: ${schemasString}. The current chat name is: ${chatName || 'unnamed'}` + : undefined + + const coreMessages: ModelMessage[] = [ + { + role: 'system', + content: system, + ...(promptProviderOptions && { + providerOptions: promptProviderOptions, + }), + }, + ...(assistantContent + ? [ + { + role: 'assistant' as const, + // Add any dynamic context here + content: assistantContent, + }, + ] + : []), + ...convertToModelMessages(messages), + ] + + return streamText({ + model, + stopWhen: stepCountIs(5), + messages: coreMessages, + ...(providerOptions && { providerOptions }), + tools, + ...(abortSignal && { abortSignal }), + }) +} diff --git a/apps/studio/pages/api/ai/sql/generate-v4.ts b/apps/studio/pages/api/ai/sql/generate-v4.ts index de6672ea412..466734aa823 100644 --- a/apps/studio/pages/api/ai/sql/generate-v4.ts +++ b/apps/studio/pages/api/ai/sql/generate-v4.ts @@ -1,36 +1,15 @@ import pgMeta from '@supabase/pg-meta' -import { - convertToModelMessages, - generateText, - isToolUIPart, - type LanguageModel, - type ModelMessage, - stepCountIs, - streamText, - type ToolSet, - type UIMessage, -} from 'ai' -import { source } from 'common-tags' import type { NextApiRequest, NextApiResponse } from 'next' import z from 'zod' import { IS_PLATFORM } from 'common' import { executeSql } from 'data/sql/execute-sql-query' +import type { UIMessage } from 'ai' import type { AiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi' import { getModel } from 'lib/ai/model' import { getOrgAIDetails } from 'lib/ai/org-ai-details' -import { - CHAT_PROMPT, - EDGE_FUNCTION_PROMPT, - GENERAL_PROMPT, - PG_BEST_PRACTICES, - RLS_PROMPT, - REALTIME_PROMPT, - SECURITY_PROMPT, - LIMITATIONS_PROMPT, -} from 'lib/ai/prompts' +import { generateAssistantResponse } from 'lib/ai/generate-assistant-response' import { getTools } from 'lib/ai/tools' -import { sanitizeMessagePart } from 'lib/ai/tools/tool-sanitizer' import apiWrapper from 'lib/api/apiWrapper' import { executeQuery } from 'lib/api/self-hosted/query' @@ -64,109 +43,6 @@ const wrapper = (req: NextApiRequest, res: NextApiResponse) => export default wrapper -export async function generateAssistantResponse({ - messages: rawMessages, - model, - tools, - aiOptInLevel = 'schema', - getSchemas, - projectRef, - chatName, - promptProviderOptions, - providerOptions, - abortSignal, -}: { - messages: UIMessage[] - model: LanguageModel - tools: ToolSet - aiOptInLevel?: AiOptInLevel - getSchemas?: () => Promise - projectRef?: string - chatName?: string - promptProviderOptions?: Record - providerOptions?: Record - abortSignal?: AbortSignal -}) { - // Only returns last 7 messages - // Filters out tools with invalid states - // Filters out tool outputs based on opt-in level using renderingToolOutputParser - const messages = (rawMessages || []).slice(-7).map((msg) => { - if (msg && msg.role === 'assistant' && 'results' in msg) { - const cleanedMsg = { ...msg } - delete cleanedMsg.results - return cleanedMsg - } - if (msg && msg.role === 'assistant' && msg.parts) { - const cleanedParts = msg.parts - .filter((part) => { - if (isToolUIPart(part)) { - const invalidStates = ['input-streaming', 'input-available', 'output-error'] - return !invalidStates.includes(part.state) - } - return true - }) - .map((part) => { - return sanitizeMessagePart(part, aiOptInLevel) - }) - return { ...msg, parts: cleanedParts } - } - return msg - }) - - const schemasString = - aiOptInLevel !== 'disabled' && getSchemas - ? await getSchemas() - : "You don't have access to any schemas." - - // Important: do not use dynamic content in the system prompt or Bedrock will not cache it - const system = source` - ${GENERAL_PROMPT} - ${CHAT_PROMPT} - ${PG_BEST_PRACTICES} - ${RLS_PROMPT} - ${EDGE_FUNCTION_PROMPT} - ${REALTIME_PROMPT} - ${SECURITY_PROMPT} - ${LIMITATIONS_PROMPT} - ` - - // Note: these must be of type `CoreMessage` to prevent AI SDK from stripping `providerOptions` - // https://github.com/vercel/ai/blob/81ef2511311e8af34d75e37fc8204a82e775e8c3/packages/ai/core/prompt/standardize-prompt.ts#L83-L88 - const assistantContent = - projectRef || chatName || schemasString !== "You don't have access to any schemas." - ? `The user's current project is ${projectRef || 'unknown'}. Their available schemas are: ${schemasString}. The current chat name is: ${chatName || 'unnamed'}` - : undefined - - const coreMessages: ModelMessage[] = [ - { - role: 'system', - content: system, - ...(promptProviderOptions && { - providerOptions: promptProviderOptions, - }), - }, - ...(assistantContent - ? [ - { - role: 'assistant' as const, - // Add any dynamic context here - content: assistantContent, - }, - ] - : []), - ...convertToModelMessages(messages), - ] - - return streamText({ - model, - stopWhen: stepCountIs(5), - messages: coreMessages, - ...(providerOptions && { providerOptions }), - tools, - ...(abortSignal && { abortSignal }), - }) -} - const requestBodySchema = z.object({ messages: z.array(z.custom()), projectRef: z.string(),