diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 940ee5e6d6..53cd319d55 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -480,7 +480,8 @@ class AIChatManager { reply += token }, onMessageEnd: () => {}, - setToolStatus: () => {} + setToolStatus: () => {}, + removeToolStatus: () => {} }, systemMessage } @@ -680,6 +681,15 @@ class AIChatManager { this.displayMessages.push(newMessage) } }, + removeToolStatus: (id) => { + const existingIdx = this.displayMessages.findIndex( + (m) => m.role === 'tool' && m.tool_call_id === id + ) + if (existingIdx !== -1) { + this.displayMessages.splice(existingIdx, 1) + this.displayMessages = [...this.displayMessages] + } + }, requestConfirmation: this.requestConfirmation } } diff --git a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte index 4a417dad31..5db4f97857 100644 --- a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte @@ -12,7 +12,7 @@ let { message }: Props = $props() - let isExpanded = $state(message.showDetails || (message.isLoading && message.needsConfirmation)) + let isExpanded = $derived(message.showDetails || (message.isLoading && message.needsConfirmation)) const hasParameters = $derived( message.parameters !== undefined && Object.keys(message.parameters).length > 0 diff --git a/frontend/src/lib/components/copilot/chat/anthropic.ts b/frontend/src/lib/components/copilot/chat/anthropic.ts index 645cc40644..b752cefd5c 100644 --- a/frontend/src/lib/components/copilot/chat/anthropic.ts +++ b/frontend/src/lib/components/copilot/chat/anthropic.ts @@ -14,6 +14,7 @@ import type { import type { MessageStream } from '@anthropic-ai/sdk/lib/MessageStream' import { getProviderAndCompletionConfig, workspaceAIClients } from '../lib' import { processToolCall, type Tool, type ToolCallbacks } from './shared' +import { generateRandomString } from '$lib/utils' export async function getAnthropicCompletion( messages: ChatCompletionMessageParam[], @@ -60,6 +61,16 @@ export async function parseAnthropicCompletion( ): Promise { let toolCallsToProcess: ChatCompletionMessageFunctionToolCall[] = [] let error = null + let tempToolId: string | undefined = undefined + + // When we receive a JSON input, we need to show a temporary tool call in loading state + completion.on('inputJson', (_: string) => { + if (!tempToolId) { + callbacks.onMessageEnd() + tempToolId = `temp-${generateRandomString(12)}` + callbacks.setToolStatus(tempToolId, { isLoading: true, content: 'Calling tool...' }) + } + }) // Handle text streaming completion.on('text', (textDelta: string, _textSnapshot: string) => { @@ -75,6 +86,11 @@ export async function parseAnthropicCompletion( addedMessages.push(assistantMessage) callbacks.onMessageEnd() } else if (block.type === 'tool_use') { + // Remove temp display if it exists + if (tempToolId) { + callbacks.removeToolStatus(tempToolId) + } + // Convert Anthropic tool calls to OpenAI format for compatibility toolCallsToProcess.push({ id: block.id, @@ -91,10 +107,17 @@ export async function parseAnthropicCompletion( } } } + + // Clear temp tracking after processing + tempToolId = undefined }) // Handle errors completion.on('error', (e: any) => { + if (tempToolId) { + callbacks.removeToolStatus(tempToolId) + tempToolId = undefined + } console.error('Anthropic stream error:', e) error = e }) diff --git a/frontend/src/lib/components/copilot/chat/shared.ts b/frontend/src/lib/components/copilot/chat/shared.ts index 6ab3cbfd31..655f44ddc0 100644 --- a/frontend/src/lib/components/copilot/chat/shared.ts +++ b/frontend/src/lib/components/copilot/chat/shared.ts @@ -269,7 +269,6 @@ export async function processToolCall({ // Check if tool requires confirmation const needsConfirmation = tool?.requiresConfirmation - // Add the tool to the display with appropriate status toolCallbacks.setToolStatus(toolCall.id, { ...(tool?.requiresConfirmation ? { content: tool.confirmationMessage ?? 'Waiting for confirmation...' } @@ -363,6 +362,7 @@ export interface Tool { export interface ToolCallbacks { setToolStatus: (id: string, metadata?: Partial) => void + removeToolStatus: (id: string) => void requestConfirmation?: (toolId: string) => Promise } diff --git a/frontend/src/lib/components/copilot/lib.ts b/frontend/src/lib/components/copilot/lib.ts index 7e18c29b14..e589f2d751 100644 --- a/frontend/src/lib/components/copilot/lib.ts +++ b/frontend/src/lib/components/copilot/lib.ts @@ -1,10 +1,5 @@ import type { AIProvider, AIProviderModel } from '$lib/gen' -import { - workspaceStore, - type DBSchema, - type GraphqlSchema, - type SQLSchema -} from '$lib/stores' +import { workspaceStore, type DBSchema, type GraphqlSchema, type SQLSchema } from '$lib/stores' import { buildClientSchema, printSchema } from 'graphql' import OpenAI from 'openai' import type { @@ -213,16 +208,16 @@ function getModelSpecificConfig( return { ...(modelProvider.model.endsWith('/thinking') ? { - thinking: { - type: 'enabled', - budget_tokens: 1024 - }, - model: modelProvider.model.slice(0, -9) - } + thinking: { + type: 'enabled', + budget_tokens: 1024 + }, + model: modelProvider.model.slice(0, -9) + } : { - model: modelProvider.model, - temperature: 0 - }), + model: modelProvider.model, + temperature: 0 + }), ...(tools && tools.length > 0 ? { tools } : {}), max_tokens: maxTokens } @@ -553,8 +548,8 @@ export function getProviderAndCompletionConfig({ }): { provider: AIProvider config: K extends true - ? ChatCompletionCreateParamsStreaming - : ChatCompletionCreateParamsNonStreaming + ? ChatCompletionCreateParamsStreaming + : ChatCompletionCreateParamsNonStreaming } { const modelProvider = forceModelProvider ?? getCurrentModel() const providerConfig = PROVIDER_COMPLETION_CONFIG_MAP[modelProvider.provider] @@ -612,13 +607,13 @@ export async function getNonStreamingCompletion( } const openaiClient = testOptions?.apiKey ? new OpenAI({ - baseURL: `${location.origin}${OpenAPI.BASE}/ai/proxy`, - apiKey: 'fake-key', - defaultHeaders: { - Authorization: '' // a non empty string will be unable to access Windmill backend proxy - }, - dangerouslyAllowBrowser: true - }) + baseURL: `${location.origin}${OpenAPI.BASE}/ai/proxy`, + apiKey: 'fake-key', + defaultHeaders: { + Authorization: '' // a non empty string will be unable to access Windmill backend proxy + }, + dangerouslyAllowBrowser: true + }) : workspaceAIClients.getOpenaiClient() const completion = await openaiClient.chat.completions.create(config, fetchOptions) @@ -797,6 +792,12 @@ export async function parseOpenAICompletion( if (tool && tool.preAction) { tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId }) } + + // Display tool call immediately in loading state + callbacks.setToolStatus(toolCallId, { + isLoading: true, + content: `Calling ${funcName} tool...` + }) } } } diff --git a/frontend/src/lib/components/workspaceSettings/AISettings.svelte b/frontend/src/lib/components/workspaceSettings/AISettings.svelte index 0c006b2c40..0a997fd43d 100644 --- a/frontend/src/lib/components/workspaceSettings/AISettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/AISettings.svelte @@ -207,7 +207,7 @@ {#if provider === 'anthropic'} Recommended - + Anthropic models handle tool calls better than other providers, which makes them a better choice for AI chat.