mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 21:29:08 +03:00
add tools
This commit is contained in:
1 parent
67c14215ef
commit
8a12ffdfc0
4 files changed
+227
No files matched your search
@@ -272,6 +272,13 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
message={message}
|
||||
status={chatStatus}
|
||||
onResults={updateMessage}
|
||||
addToolResult={
|
||||
addToolResult as unknown as (args: {
|
||||
tool: string
|
||||
toolCallId: string
|
||||
output: unknown
|
||||
}) => Promise<void>
|
||||
}
|
||||
onDelete={deleteMessageFromHere}
|
||||
onEdit={editMessage}
|
||||
isAfterEditedMessage={isAfterEditedMessage}
|
||||
@@ -288,6 +295,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
cancelEdit,
|
||||
editingMessageId,
|
||||
chatStatus,
|
||||
addToolResult,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ import remarkGfm from 'remark-gfm'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { ProfileImage } from 'components/ui/ProfileImage'
|
||||
import { useParams } from 'common'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { constructHeaders } from 'data/fetchers'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { useEdgeFunctionDeployMutation } from 'data/edge-functions/edge-functions-deploy-mutation'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { cn, markdownComponents, WarningIcon } from 'ui'
|
||||
import { ButtonTooltip } from '../ButtonTooltip'
|
||||
@@ -55,6 +60,7 @@ interface MessageProps {
|
||||
resultId?: string
|
||||
results: any[]
|
||||
}) => void
|
||||
addToolResult?: (args: { tool: string; toolCallId: string; output: unknown }) => Promise<void>
|
||||
onDelete: (id: string) => void
|
||||
onEdit: (id: string) => void
|
||||
isAfterEditedMessage: boolean
|
||||
@@ -70,6 +76,7 @@ const Message = function Message({
|
||||
action = null,
|
||||
variant = 'default',
|
||||
onResults,
|
||||
addToolResult,
|
||||
onDelete,
|
||||
onEdit,
|
||||
isAfterEditedMessage = false,
|
||||
@@ -78,6 +85,9 @@ const Message = function Message({
|
||||
onCancelEdit,
|
||||
}: PropsWithChildren<MessageProps>) {
|
||||
const { profile } = useProfile()
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { mutateAsync: deployFunction } = useEdgeFunctionDeployMutation()
|
||||
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false)
|
||||
const allMarkdownComponents: Partial<Components> = useMemo(
|
||||
() => ({
|
||||
@@ -220,6 +230,191 @@ const Message = function Message({
|
||||
}
|
||||
return null
|
||||
}
|
||||
case 'tool-execute_sql': {
|
||||
const { toolCallId, state, input } = part
|
||||
if (state === 'input-streaming') {
|
||||
return (
|
||||
<div
|
||||
key={`${id}-tool-loading-execute_sql`}
|
||||
className="rounded-lg border bg-surface-75 font-mono text-xs text-foreground-lighter py-2 px-3 flex items-center gap-2"
|
||||
>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{`Preparing SQL execution...`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (state === 'input-available') {
|
||||
return (
|
||||
<div
|
||||
key={`${id}-tool-${toolCallId}`}
|
||||
className="rounded-lg border bg-surface-75 text-xs text-foreground p-3 my-3"
|
||||
>
|
||||
<div className="mb-2 font-mono text-foreground-light">
|
||||
About to run:
|
||||
</div>
|
||||
<pre className="overflow-x-auto text-xs bg-background p-2 rounded border">
|
||||
{(input as any).sql}
|
||||
</pre>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button
|
||||
className="text-xs px-2 py-1 border rounded hover:bg-surface-100"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const headers = await constructHeaders()
|
||||
const { result } = await executeSql<any[]>(
|
||||
{
|
||||
projectRef: ref,
|
||||
connectionString: project?.connectionString,
|
||||
sql: (input as any).sql,
|
||||
},
|
||||
undefined,
|
||||
{ Authorization: headers.get('Authorization') ?? '' }
|
||||
)
|
||||
|
||||
const rows = Array.isArray(result) ? result : []
|
||||
onResults({
|
||||
messageId: id,
|
||||
resultId: toolCallId,
|
||||
results: rows,
|
||||
})
|
||||
await addToolResult?.({
|
||||
tool: 'execute_sql',
|
||||
toolCallId,
|
||||
output: rows,
|
||||
})
|
||||
} catch (e: any) {
|
||||
await addToolResult?.({
|
||||
tool: 'execute_sql',
|
||||
toolCallId,
|
||||
output: `Error: ${e?.message ?? String(e)}`,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
Run SQL
|
||||
</button>
|
||||
<button
|
||||
className="text-xs px-2 py-1 border rounded hover:bg-surface-100"
|
||||
onClick={async () => {
|
||||
await addToolResult?.({
|
||||
tool: 'execute_sql',
|
||||
toolCallId,
|
||||
output: 'Cancelled',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (state === 'output-available') {
|
||||
return (
|
||||
<div
|
||||
key={`${id}-tool-${toolCallId}`}
|
||||
className="text-xs text-foreground"
|
||||
>
|
||||
SQL executed.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (state === 'output-error') {
|
||||
return (
|
||||
<div key={`${id}-tool-${toolCallId}`} className="text-xs text-danger">
|
||||
Failed to execute SQL.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
case 'tool-deploy_edge_function': {
|
||||
const { toolCallId, state, input } = part
|
||||
if (state === 'input-streaming') {
|
||||
return (
|
||||
<div
|
||||
key={`${id}-tool-loading-deploy_edge_function`}
|
||||
className="rounded-lg border bg-surface-75 font-mono text-xs text-foreground-lighter py-2 px-3 flex items-center gap-2"
|
||||
>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{`Preparing Edge Function deployment...`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (state === 'input-available' || state === 'output-available') {
|
||||
return (
|
||||
<div
|
||||
key={`${id}-tool-${toolCallId}`}
|
||||
className="w-auto overflow-x-hidden my-4"
|
||||
>
|
||||
<EdgeFunctionBlock
|
||||
label={(input as any).name || 'Edge Function'}
|
||||
code={(input as any).code}
|
||||
functionName={(input as any).name || 'my-function'}
|
||||
showCode={!readOnly}
|
||||
actions={
|
||||
state === 'input-available' ? (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="text-xs px-2 py-1 border rounded hover:bg-surface-100"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deployFunction({
|
||||
projectRef: ref!,
|
||||
slug: (input as any).name || 'my-function',
|
||||
metadata: {
|
||||
entrypoint_path: 'index.ts',
|
||||
name: (input as any).name || 'my-function',
|
||||
verify_jwt: true,
|
||||
},
|
||||
files: [
|
||||
{ name: 'index.ts', content: (input as any).code },
|
||||
],
|
||||
})
|
||||
await addToolResult?.({
|
||||
tool: 'deploy_edge_function',
|
||||
toolCallId,
|
||||
output: 'Deployed successfully',
|
||||
})
|
||||
} catch (e: any) {
|
||||
await addToolResult?.({
|
||||
tool: 'deploy_edge_function',
|
||||
toolCallId,
|
||||
output: `Error: ${e?.message ?? String(e)}`,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
Confirm deploy
|
||||
</button>
|
||||
<button
|
||||
className="text-xs px-2 py-1 border rounded hover:bg-surface-100"
|
||||
onClick={async () => {
|
||||
await addToolResult?.({
|
||||
tool: 'deploy_edge_function',
|
||||
toolCallId,
|
||||
output: 'Cancelled',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (state === 'output-error') {
|
||||
return (
|
||||
<div key={`${id}-tool-${toolCallId}`} className="text-xs text-danger">
|
||||
Failed to deploy Edge Function.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
case 'tool-display_edge_function': {
|
||||
const { toolCallId, state, input } = part
|
||||
if (state === 'input-streaming' || state === 'input-available') {
|
||||
@@ -329,6 +524,7 @@ export const MemoizedMessage = memo(
|
||||
message,
|
||||
status,
|
||||
onResults,
|
||||
addToolResult,
|
||||
onDelete,
|
||||
onEdit,
|
||||
isAfterEditedMessage,
|
||||
@@ -346,6 +542,7 @@ export const MemoizedMessage = memo(
|
||||
resultId?: string
|
||||
results: any[]
|
||||
}) => void
|
||||
addToolResult?: (args: { tool: string; toolCallId: string; output: unknown }) => Promise<void>
|
||||
onDelete: (id: string) => void
|
||||
onEdit: (id: string) => void
|
||||
isAfterEditedMessage: boolean
|
||||
@@ -360,6 +557,7 @@ export const MemoizedMessage = memo(
|
||||
isLoading={status === 'submitted' || status === 'streaming'}
|
||||
status={status}
|
||||
onResults={onResults}
|
||||
addToolResult={addToolResult}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
isAfterEditedMessage={isAfterEditedMessage}
|
||||
|
||||
@@ -27,6 +27,8 @@ export const toolSetValidationSchema = z.record(
|
||||
'get_advisors',
|
||||
|
||||
// Local tools
|
||||
'execute_sql',
|
||||
'deploy_edge_function',
|
||||
'display_query',
|
||||
'display_edge_function',
|
||||
'rename_chat',
|
||||
@@ -87,6 +89,8 @@ type ToolCategory = (typeof TOOL_CATEGORIES)[keyof typeof TOOL_CATEGORIES]
|
||||
*/
|
||||
export const TOOL_CATEGORY_MAP: Record<string, ToolCategory> = {
|
||||
// UI tools - always available
|
||||
execute_sql: TOOL_CATEGORIES.UI,
|
||||
deploy_edge_function: TOOL_CATEGORIES.UI,
|
||||
display_query: TOOL_CATEGORIES.UI,
|
||||
display_edge_function: TOOL_CATEGORIES.UI,
|
||||
rename_chat: TOOL_CATEGORIES.UI,
|
||||
|
||||
@@ -2,6 +2,23 @@ import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const getRenderingTools = () => ({
|
||||
execute_sql: tool({
|
||||
description:
|
||||
'Execute a SQL statement on the client. Use for SELECT/DDL/DML. Client will confirm before running and return results.',
|
||||
inputSchema: z.object({
|
||||
sql: z.string().describe('The SQL statement to execute.'),
|
||||
}),
|
||||
// No execute – runs client-side in the UI
|
||||
}),
|
||||
deploy_edge_function: tool({
|
||||
description:
|
||||
'Deploy a Supabase Edge Function from provided code on the client. Client will confirm before deploying and return the result.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The URL-friendly name/slug of the Edge Function.'),
|
||||
code: z.string().describe('The TypeScript code for the Edge Function.'),
|
||||
}),
|
||||
// No execute – runs client-side in the UI
|
||||
}),
|
||||
display_query: tool({
|
||||
description:
|
||||
'Displays SQL query results (table or chart) or renders SQL for write/DDL operations. Use this for all query display needs. Optionally references a previous execute_sql call via manualToolCallId for displaying SELECT results.',
|
||||
|
||||
Reference in new issue
Block a user