mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Chore / dependency upgrade. ## What is the current behavior? Studio is on AI SDK 6 (`ai` ^6.0.174, `@ai-sdk/react` ^3). Tool approvals still use the v6 `needsApproval` flag on individual tools. ## What is the new behavior? Upgrades Studio to AI SDK 7 (`ai` 7.0.59) and the matching `@ai-sdk/*` packages. Aligns call sites with v7 names (`instructions`, `isStepCount`, `onEnd`, `ToolExecutionOptions`). This is the bottom of stack #49171. Later layers add a shared Confirm card and AssistantQueryCell. ## Additional context - Stack: #49167 → #49168 → #49169 → #49170 - `needsApproval` on tools is left as-is in this PR so the upgrade can land independently. A follow-up can move those gates to `streamText({ toolApproval })` and `experimental_toolApprovalSecret`. - Independent of the notebook preview stack ([#49112](https://github.com/supabase/supabase/pull/49112), [#49159](https://github.com/supabase/supabase/pull/49159)), which should merge first before we wrap notebook proposals in Confirm. ## Test plan - [ ] `pnpm --filter studio test` for `lib/ai/tools/*` and assistant generate path - [ ] Assistant chat still streams and tool-approval SQL / Edge Function still pause for confirm - [ ] Evals still run with mock tools (`needsApproval: false` overrides) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Updated AI-powered chat, onboarding, SQL, code completion, and recipe generation workflows for more reliable responses. * Streaming responses now better preserve reasoning and source information where available. * Improved tool privacy notices while preserving dynamically generated tool descriptions. * Refined AI response handling, including step limits and structured policy results. * **Bug Fixes** * Improved compatibility across AI-powered tool interactions and execution scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
215 lines
6.6 KiB
TypeScript
215 lines
6.6 KiB
TypeScript
import * as ai from 'ai'
|
|
import {
|
|
convertToModelMessages,
|
|
isStepCount,
|
|
isToolUIPart,
|
|
type LanguageModel,
|
|
type ModelMessage,
|
|
type SystemModelMessage,
|
|
type ToolSet,
|
|
type UIMessage,
|
|
} from 'ai'
|
|
import { startSpan, traced, withCurrent, wrapAISDK, type Span } from 'braintrust'
|
|
import { source } from 'common-tags'
|
|
|
|
import type { AssistantEvalInput } from '@/evals/scorer'
|
|
import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
|
|
import { buildAssistantContextMessages, NO_SCHEMA_ACCESS_MESSAGE } from '@/lib/ai/assistant-context'
|
|
import { IS_TRACING_ENABLED } from '@/lib/ai/braintrust-logger'
|
|
import {
|
|
CHAT_PROMPT,
|
|
GENERAL_PROMPT,
|
|
LIMITATIONS_PROMPT,
|
|
NOTEBOOKS_PROMPT,
|
|
SECURITY_PROMPT,
|
|
} from '@/lib/ai/prompts'
|
|
import { sanitizeMessagePart } from '@/lib/ai/tools/tool-sanitizer'
|
|
|
|
const { streamText: tracedStreamText } = wrapAISDK(ai)
|
|
|
|
export async function generateAssistantResponse({
|
|
messages: rawMessages,
|
|
model,
|
|
tools,
|
|
aiOptInLevel = 'schema',
|
|
getSchemas,
|
|
projectRef,
|
|
chatId,
|
|
chatName,
|
|
allowTracing,
|
|
supportMode,
|
|
userId,
|
|
orgId,
|
|
planId,
|
|
includesLogsSnippets,
|
|
isExplorerEnabled,
|
|
systemProviderOptions,
|
|
providerOptions,
|
|
requestedModel,
|
|
abortSignal,
|
|
onSpanCreated,
|
|
}: {
|
|
messages: UIMessage[]
|
|
model: LanguageModel
|
|
tools: ToolSet
|
|
aiOptInLevel?: AiOptInLevel
|
|
getSchemas?: () => Promise<string>
|
|
projectRef?: string
|
|
chatId?: string
|
|
chatName?: string
|
|
allowTracing?: boolean
|
|
supportMode?: boolean
|
|
userId?: string
|
|
orgId?: number
|
|
planId?: string
|
|
/** Whether any user message in the conversation attached a logs (ClickHouse) query. */
|
|
includesLogsSnippets?: boolean
|
|
isExplorerEnabled?: boolean
|
|
requestedModel?: string
|
|
systemProviderOptions?: Record<string, any>
|
|
providerOptions?: Record<string, any>
|
|
abortSignal?: AbortSignal
|
|
onSpanCreated?: (spanId: string) => void
|
|
}) {
|
|
const shouldTrace = allowTracing ?? IS_TRACING_ENABLED
|
|
|
|
const run = async (span?: Span) => {
|
|
// Only returns last 7 messages
|
|
// Filters out tools with invalid states
|
|
// Filters out tool outputs based on opt-in level
|
|
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',
|
|
'approval-requested',
|
|
'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
|
|
? shouldTrace
|
|
? await traced(async () => getSchemas(), { name: 'getSchemas', type: 'function' })
|
|
: await getSchemas()
|
|
: NO_SCHEMA_ACCESS_MESSAGE
|
|
|
|
// Important: do not use per-request dynamic content in the system prompt or Bedrock will
|
|
// not cache it. isExplorerEnabled is a per-user flag, not per-request, so it only produces
|
|
// two prompt variants (on/off) rather than defeating caching.
|
|
const system = source`
|
|
${GENERAL_PROMPT}
|
|
${CHAT_PROMPT}
|
|
${isExplorerEnabled ? NOTEBOOKS_PROMPT : ''}
|
|
${SECURITY_PROMPT}
|
|
${LIMITATIONS_PROMPT}
|
|
|
|
## Available Knowledge
|
|
|
|
Before writing SQL or answering questions about the following topics, call \`load_knowledge\` to load detailed knowledge:
|
|
- \`pg_best_practices\` — PostgreSQL best practices. Always load before writing any SQL, even simple queries.
|
|
- \`rls\` — Row Level Security policies for database tables.
|
|
- \`storage\` — Supabase Storage buckets, public/private bucket access, and \`storage.objects\` policies. Always load before creating Storage buckets or \`storage.objects\` policies.
|
|
- \`edge_functions\` — Supabase Edge Functions
|
|
- \`realtime\` — Supabase Realtime
|
|
`
|
|
|
|
const systemMessage: SystemModelMessage = {
|
|
role: 'system',
|
|
content: system,
|
|
...(systemProviderOptions && { providerOptions: systemProviderOptions }),
|
|
}
|
|
|
|
const coreMessages: ModelMessage[] = [
|
|
...buildAssistantContextMessages({
|
|
projectRef,
|
|
chatName,
|
|
schemasString,
|
|
supportMode,
|
|
includesLogsSnippets,
|
|
}),
|
|
...(await convertToModelMessages(messages)),
|
|
]
|
|
|
|
const streamTextFn = shouldTrace ? tracedStreamText : ai.streamText
|
|
|
|
return streamTextFn({
|
|
model,
|
|
instructions: systemMessage,
|
|
stopWhen: isStepCount(10),
|
|
messages: coreMessages,
|
|
...(providerOptions && { providerOptions }),
|
|
tools,
|
|
...(abortSignal && { abortSignal }),
|
|
...(span && {
|
|
onEnd: ({ steps, finishReason }) => {
|
|
const metadata: Record<string, unknown> = {
|
|
isFinalStep: finishReason === 'stop',
|
|
}
|
|
for (const step of steps) {
|
|
for (const toolCall of step.toolCalls) {
|
|
if (toolCall.toolName === 'rename_chat') {
|
|
const { newName } = toolCall.input as { newName: string }
|
|
metadata.chatName = newName
|
|
}
|
|
}
|
|
}
|
|
span.log({ metadata })
|
|
span.end()
|
|
},
|
|
}),
|
|
} satisfies Parameters<typeof ai.streamText>[0])
|
|
}
|
|
|
|
if (shouldTrace) {
|
|
// startSpan instead of traced() so we control when the span closes via onEnd.
|
|
// Scorers read from child spans (LLM + tool) in the trace rather than a root span output field.
|
|
const span = startSpan({ name: 'generateAssistantResponse', type: 'function' })
|
|
onSpanCreated?.(span.id)
|
|
|
|
const lastUserMessage = rawMessages.findLast((m) => m.role === 'user')
|
|
const lastUserText = lastUserMessage?.parts
|
|
?.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
|
|
.map((p) => p.text)
|
|
.join('\n')
|
|
|
|
span.log({
|
|
input: { prompt: lastUserText ?? '' } satisfies AssistantEvalInput,
|
|
metadata: {
|
|
projectRef,
|
|
chatId,
|
|
chatName,
|
|
aiOptInLevel,
|
|
userId,
|
|
orgId,
|
|
planId,
|
|
requestedModel,
|
|
gitBranch: process.env.VERCEL_GIT_COMMIT_REF,
|
|
environment: process.env.NEXT_PUBLIC_ENVIRONMENT,
|
|
},
|
|
})
|
|
|
|
return withCurrent(span, () => run(span))
|
|
}
|
|
|
|
return run()
|
|
}
|