mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 00:00:46 +00:00
chore(aichat): display tool usage earlier (#6917)
* feat(copilot): display tool calls immediately in loading state during streaming Display tool calls in loading state as soon as they are parsed during OpenAI streaming, rather than waiting until processToolCall is invoked. Changes: - parseOpenAICompletion: Track initialized tool calls and display them immediately when we have complete tool info (id + function.name) - processToolCall: Updated comment to clarify it merges with existing loading state set during parsing This provides better UX by showing tool execution progress progressively as the stream is parsed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(copilot): display Anthropic tool calls immediately in loading state Apply the same immediate tool call display pattern to Anthropic streaming that was implemented for OpenAI. Changes: - parseAnthropicCompletion: Display tool calls immediately in loading state when tool_use blocks are received in the message event This ensures consistent UX across both OpenAI and Anthropic providers, showing tool execution progress as soon as tool calls are detected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(copilot): show Anthropic tool calls even earlier with temp displays Display temporary loading states for Anthropic tool calls as soon as inputJson events are received (when tool input starts streaming), then replace them with real tool displays when complete tool_use blocks arrive in the message event. Changes: - ToolCallbacks: Added removeToolStatus method to clean up temp displays - AIChatManager: Implemented removeToolStatus to remove tool messages from displayMessages array - anthropic.ts: * Display temp tool on first inputJson event (earliest indicator) * Flush pending text message before showing temp tool (proper ordering) * Remove temp display when complete tool_use block arrives * Replace with real tool display via preAction This provides the earliest possible feedback for Anthropic tool calls, showing loading states as soon as the model starts generating tool inputs rather than waiting for complete blocks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * cleaning * cleaning * cleaning * fix icon * nit * handle error * nit --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<boolean> {
|
||||
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
|
||||
})
|
||||
|
||||
@@ -269,7 +269,6 @@ export async function processToolCall<T>({
|
||||
// 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<T> {
|
||||
|
||||
export interface ToolCallbacks {
|
||||
setToolStatus: (id: string, metadata?: Partial<ToolDisplayMessage>) => void
|
||||
removeToolStatus: (id: string) => void
|
||||
requestConfirmation?: (toolId: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
|
||||
@@ -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<K extends boolean>({
|
||||
}): {
|
||||
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...`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
{#if provider === 'anthropic'}
|
||||
<Badge color="blue">
|
||||
Recommended
|
||||
<Tooltip class="text-blue-800 dark:text-blue-800 mt-0.5">
|
||||
<Tooltip>
|
||||
Anthropic models handle tool calls better than other providers, which makes them a
|
||||
better choice for AI chat.
|
||||
</Tooltip>
|
||||
|
||||
Reference in New Issue
Block a user