mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 08:00:45 +00:00
draft
This commit is contained in:
@@ -11,16 +11,13 @@ import HistoryManager from './HistoryManager.svelte'
|
||||
import {
|
||||
extractCodeFromMarkdown,
|
||||
getLatestAssistantMessage,
|
||||
processToolCall,
|
||||
type DisplayMessage,
|
||||
type Tool,
|
||||
type ToolCallbacks,
|
||||
type ToolDisplayMessage
|
||||
} from './shared'
|
||||
import type {
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageToolCall,
|
||||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionUserMessageParam
|
||||
} from 'openai/resources/chat/completions.mjs'
|
||||
@@ -34,7 +31,7 @@ import { loadApiTools } from './api/apiTools'
|
||||
import { prepareScriptUserMessage } from './script/core'
|
||||
import { prepareNavigatorUserMessage } from './navigator/core'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getCompletion, getModelContextWindow } from '../lib'
|
||||
import { getCompletion, getModelContextWindow, parseOpenAICompletion } from '../lib'
|
||||
import { dfs } from '$lib/components/flows/previousResults'
|
||||
import { getStringError } from './utils'
|
||||
import type { FlowModuleState, FlowState } from '$lib/components/flows/flowState'
|
||||
@@ -380,10 +377,8 @@ class AIChatManager {
|
||||
}
|
||||
systemMessage?: ChatCompletionSystemMessageParam
|
||||
}) => {
|
||||
let addedMessages: ChatCompletionMessageParam[] = []
|
||||
try {
|
||||
let completion: any = null
|
||||
|
||||
let addedMessages: ChatCompletionMessageParam[] = []
|
||||
while (true) {
|
||||
const systemMessage = systemMessageOverride ?? this.systemMessage
|
||||
const helpers = this.helpers
|
||||
@@ -413,99 +408,23 @@ class AIChatManager {
|
||||
}
|
||||
this.pendingPrompt = ''
|
||||
}
|
||||
completion = await getCompletion(
|
||||
|
||||
const completion = await getCompletion(
|
||||
[systemMessage, ...messages, ...(pendingUserMessage ? [pendingUserMessage] : [])],
|
||||
abortController,
|
||||
tools.map((t) => t.def)
|
||||
)
|
||||
|
||||
if (completion) {
|
||||
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
|
||||
|
||||
let answer = ''
|
||||
for await (const chunk of completion) {
|
||||
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
|
||||
continue
|
||||
}
|
||||
const c = chunk as ChatCompletionChunk
|
||||
const delta = c.choices[0].delta.content
|
||||
if (delta) {
|
||||
answer += delta
|
||||
callbacks.onNewToken(delta)
|
||||
}
|
||||
const toolCalls = c.choices[0].delta.tool_calls || []
|
||||
if (toolCalls.length > 0 && answer) {
|
||||
// if tool calls are present but we have some textual content already, we need to display it to the user first
|
||||
callbacks.onMessageEnd()
|
||||
answer = ''
|
||||
}
|
||||
for (const toolCall of toolCalls) {
|
||||
const { index } = toolCall
|
||||
let finalToolCall = finalToolCalls[index]
|
||||
if (!finalToolCall) {
|
||||
finalToolCalls[index] = toolCall
|
||||
} else {
|
||||
if (toolCall.function?.arguments) {
|
||||
if (!finalToolCall.function) {
|
||||
finalToolCall.function = toolCall.function
|
||||
} else {
|
||||
finalToolCall.function.arguments =
|
||||
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
finalToolCall = finalToolCalls[index]
|
||||
if (finalToolCall?.function) {
|
||||
const {
|
||||
function: { name: funcName },
|
||||
id: toolCallId
|
||||
} = finalToolCall
|
||||
if (funcName && toolCallId) {
|
||||
const tool = tools.find((t) => t.def.function.name === funcName)
|
||||
if (tool && tool.preAction) {
|
||||
tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (answer) {
|
||||
const toAdd = { role: 'assistant' as const, content: answer }
|
||||
addedMessages.push(toAdd)
|
||||
messages.push(toAdd)
|
||||
}
|
||||
|
||||
callbacks.onMessageEnd()
|
||||
|
||||
const toolCalls = Object.values(finalToolCalls).filter(
|
||||
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
|
||||
) as ChatCompletionMessageToolCall[]
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
const toAdd = {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: toolCalls.map((t) => ({
|
||||
...t,
|
||||
function: {
|
||||
...t.function,
|
||||
arguments: t.function.arguments || '{}'
|
||||
}
|
||||
}))
|
||||
}
|
||||
messages.push(toAdd)
|
||||
addedMessages.push(toAdd)
|
||||
for (const toolCall of toolCalls) {
|
||||
const messageToAdd = await processToolCall({
|
||||
tools,
|
||||
toolCall,
|
||||
helpers,
|
||||
toolCallbacks: callbacks
|
||||
})
|
||||
messages.push(messageToAdd)
|
||||
addedMessages.push(messageToAdd)
|
||||
}
|
||||
} else {
|
||||
const continueCompletion = await parseOpenAICompletion(
|
||||
completion,
|
||||
callbacks,
|
||||
messages,
|
||||
addedMessages,
|
||||
tools,
|
||||
helpers
|
||||
)
|
||||
if (!continueCompletion) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,57 @@ import type {
|
||||
TextBlockParam,
|
||||
ToolUnion,
|
||||
ToolUseBlockParam,
|
||||
Tool
|
||||
Tool as AnthropicTool
|
||||
} from '@anthropic-ai/sdk/resources'
|
||||
import { getProviderAndCompletionConfig, workspaceAIClients } from '../lib'
|
||||
import type { Stream } from 'openai/streaming.mjs'
|
||||
import type { Tool, ToolCallbacks } from './shared'
|
||||
|
||||
export async function getAnthropicCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
|
||||
) {
|
||||
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true })
|
||||
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
|
||||
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
|
||||
|
||||
const anthropicClient = workspaceAIClients.getAnthropicClient()
|
||||
|
||||
const anthropicParams = {
|
||||
model: config.model,
|
||||
max_tokens: config.max_tokens as number,
|
||||
messages: anthropicMessages,
|
||||
...(system && { system }),
|
||||
...(anthropicTools && { tools: anthropicTools }),
|
||||
...(typeof config.temperature === 'number' && { temperature: config.temperature })
|
||||
}
|
||||
|
||||
const stream = anthropicClient.messages.stream(anthropicParams, {
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'X-Provider': provider,
|
||||
'anthropic-version': '2023-06-01'
|
||||
}
|
||||
})
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
export async function parseAnthropicCompletion(
|
||||
completion: Stream<any>,
|
||||
callbacks: ToolCallbacks & {
|
||||
onNewToken: (token: string) => void
|
||||
onMessageEnd: () => void
|
||||
},
|
||||
messages: ChatCompletionMessageParam[],
|
||||
addedMessages: ChatCompletionMessageParam[],
|
||||
tools: Tool<any>[],
|
||||
helpers: any
|
||||
): Promise<boolean> {
|
||||
// TODO: Implement Anthropic completion parsing
|
||||
return true
|
||||
}
|
||||
|
||||
export function convertOpenAIToAnthropicMessages(messages: ChatCompletionMessageParam[]): {
|
||||
system: TextBlockParam[] | undefined
|
||||
@@ -125,7 +174,7 @@ export function convertOpenAIToolsToAnthropic(
|
||||
input_schema: (tool.function.parameters || {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}) as Tool.InputSchema
|
||||
}) as AnthropicTool.InputSchema
|
||||
}))
|
||||
|
||||
// Add cache_control to the last tool to cache all tool definitions
|
||||
@@ -135,134 +184,3 @@ export function convertOpenAIToolsToAnthropic(
|
||||
|
||||
return anthropicTools
|
||||
}
|
||||
|
||||
export async function* convertAnthropicStreamToOpenAI(
|
||||
model: string,
|
||||
stream: Stream<MessageStreamEvent>
|
||||
): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {
|
||||
let currentToolCall: { id: string; name: string; args: string } | null = null
|
||||
const messageId = `chatcmpl-${Date.now()}`
|
||||
|
||||
try {
|
||||
for await (const event of stream) {
|
||||
switch (event.type) {
|
||||
case 'message_start':
|
||||
yield {
|
||||
id: messageId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }]
|
||||
}
|
||||
break
|
||||
|
||||
case 'content_block_start':
|
||||
if (event.content_block?.type === 'tool_use') {
|
||||
currentToolCall = {
|
||||
id: event.content_block.id,
|
||||
name: event.content_block.name,
|
||||
args: ''
|
||||
}
|
||||
}
|
||||
// Text and thinking blocks start here but don't need special handling
|
||||
break
|
||||
|
||||
case 'content_block_delta':
|
||||
switch (event.delta?.type) {
|
||||
case 'text_delta':
|
||||
yield {
|
||||
id: messageId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [{ index: 0, delta: { content: event.delta.text }, finish_reason: null }]
|
||||
}
|
||||
break
|
||||
|
||||
case 'input_json_delta':
|
||||
if (currentToolCall) {
|
||||
currentToolCall.args += event.delta.partial_json
|
||||
}
|
||||
break
|
||||
|
||||
case 'thinking_delta':
|
||||
// Skip thinking deltas - they're internal reasoning
|
||||
break
|
||||
|
||||
default:
|
||||
// Handle unknown delta types gracefully
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case 'content_block_stop':
|
||||
if (currentToolCall) {
|
||||
// Emit completed tool call
|
||||
yield {
|
||||
id: messageId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: currentToolCall.id,
|
||||
type: 'function' as const,
|
||||
function: { name: currentToolCall.name, arguments: currentToolCall.args }
|
||||
}
|
||||
]
|
||||
},
|
||||
finish_reason: null
|
||||
}
|
||||
]
|
||||
}
|
||||
currentToolCall = null
|
||||
}
|
||||
break
|
||||
|
||||
case 'message_delta':
|
||||
const finishReason =
|
||||
event.delta?.stop_reason === 'end_turn'
|
||||
? 'stop'
|
||||
: event.delta?.stop_reason === 'tool_use'
|
||||
? 'tool_calls'
|
||||
: event.delta?.stop_reason === 'max_tokens'
|
||||
? 'length'
|
||||
: null
|
||||
|
||||
yield {
|
||||
id: messageId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: finishReason }]
|
||||
}
|
||||
break
|
||||
|
||||
case 'message_stop':
|
||||
// Stream completed successfully
|
||||
break
|
||||
|
||||
case 'ping':
|
||||
// Ignore ping events
|
||||
break
|
||||
|
||||
case 'error':
|
||||
// Handle error events
|
||||
throw new Error(`Anthropic stream error: ${JSON.stringify(event.error)}`)
|
||||
|
||||
default:
|
||||
// Handle unknown event types gracefully as per Anthropic docs
|
||||
console.debug('Unknown Anthropic stream event type:', event.type)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing Anthropic stream:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
import { buildClientSchema, printSchema } from 'graphql'
|
||||
import { OpenAI } from 'openai'
|
||||
import type {
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionCreateParams,
|
||||
ChatCompletionCreateParamsNonStreaming,
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
ChatCompletionMessageParam
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageToolCall
|
||||
} from 'openai/resources/index.mjs'
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
@@ -20,11 +22,8 @@ import { OpenAPI, ResourceService, type Script } from '../../gen'
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
import { formatResourceTypes } from './utils'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
convertAnthropicStreamToOpenAI,
|
||||
convertOpenAIToAnthropicMessages,
|
||||
convertOpenAIToolsToAnthropic
|
||||
} from './chat/anthropic'
|
||||
import type { Stream } from 'openai/streaming.mjs'
|
||||
import { processToolCall, type Tool, type ToolCallbacks } from './chat/shared'
|
||||
|
||||
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
|
||||
|
||||
@@ -484,7 +483,7 @@ const PROMPTS_CONFIGS = {
|
||||
gen: GEN_CONFIG
|
||||
}
|
||||
|
||||
function getProviderAndCompletionConfig<K extends boolean>({
|
||||
export function getProviderAndCompletionConfig<K extends boolean>({
|
||||
messages,
|
||||
stream,
|
||||
tools,
|
||||
@@ -649,35 +648,6 @@ export async function getCompletion(
|
||||
) {
|
||||
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true, tools })
|
||||
|
||||
// If using Anthropic, use the Anthropic SDK with format conversion
|
||||
if (provider === 'anthropic') {
|
||||
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
|
||||
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
|
||||
|
||||
const anthropicClient = workspaceAIClients.getAnthropicClient()
|
||||
|
||||
const anthropicParams = {
|
||||
model: config.model,
|
||||
max_tokens: config.max_tokens as number,
|
||||
messages: anthropicMessages,
|
||||
stream: true as const,
|
||||
...(system && { system }),
|
||||
...(anthropicTools && { tools: anthropicTools }),
|
||||
...(typeof config.temperature === 'number' && { temperature: config.temperature })
|
||||
}
|
||||
|
||||
const stream = await anthropicClient.messages.create(anthropicParams, {
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'X-Provider': provider,
|
||||
'anthropic-version': '2023-06-01'
|
||||
}
|
||||
})
|
||||
|
||||
return convertAnthropicStreamToOpenAI(config.model, stream)
|
||||
}
|
||||
|
||||
// For other providers, use the existing OpenAI client
|
||||
const openaiClient = workspaceAIClients.getOpenaiClient()
|
||||
const completion = await openaiClient.chat.completions.create(config, {
|
||||
signal: abortController.signal,
|
||||
@@ -688,6 +658,108 @@ export async function getCompletion(
|
||||
return completion
|
||||
}
|
||||
|
||||
export async function parseOpenAICompletion(
|
||||
completion: Stream<any>,
|
||||
callbacks: ToolCallbacks & {
|
||||
onNewToken: (token: string) => void
|
||||
onMessageEnd: () => void
|
||||
},
|
||||
messages: ChatCompletionMessageParam[],
|
||||
addedMessages: ChatCompletionMessageParam[],
|
||||
tools: Tool<any>[],
|
||||
helpers: any
|
||||
): Promise<boolean> {
|
||||
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
|
||||
|
||||
let answer = ''
|
||||
for await (const chunk of completion) {
|
||||
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
|
||||
continue
|
||||
}
|
||||
const c = chunk as ChatCompletionChunk
|
||||
const delta = c.choices[0].delta.content
|
||||
if (delta) {
|
||||
answer += delta
|
||||
callbacks.onNewToken(delta)
|
||||
}
|
||||
const toolCalls = c.choices[0].delta.tool_calls || []
|
||||
if (toolCalls.length > 0 && answer) {
|
||||
// if tool calls are present but we have some textual content already, we need to display it to the user first
|
||||
callbacks.onMessageEnd()
|
||||
answer = ''
|
||||
}
|
||||
for (const toolCall of toolCalls) {
|
||||
const { index } = toolCall
|
||||
let finalToolCall = finalToolCalls[index]
|
||||
if (!finalToolCall) {
|
||||
finalToolCalls[index] = toolCall
|
||||
} else {
|
||||
if (toolCall.function?.arguments) {
|
||||
if (!finalToolCall.function) {
|
||||
finalToolCall.function = toolCall.function
|
||||
} else {
|
||||
finalToolCall.function.arguments =
|
||||
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
finalToolCall = finalToolCalls[index]
|
||||
if (finalToolCall?.function) {
|
||||
const {
|
||||
function: { name: funcName },
|
||||
id: toolCallId
|
||||
} = finalToolCall
|
||||
if (funcName && toolCallId) {
|
||||
const tool = tools.find((t) => t.def.function.name === funcName)
|
||||
if (tool && tool.preAction) {
|
||||
tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (answer) {
|
||||
const toAdd = { role: 'assistant' as const, content: answer }
|
||||
addedMessages.push(toAdd)
|
||||
messages.push(toAdd)
|
||||
}
|
||||
|
||||
callbacks.onMessageEnd()
|
||||
|
||||
const toolCalls = Object.values(finalToolCalls).filter(
|
||||
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
|
||||
) as ChatCompletionMessageToolCall[]
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
const toAdd = {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: toolCalls.map((t) => ({
|
||||
...t,
|
||||
function: {
|
||||
...t.function,
|
||||
arguments: t.function.arguments || '{}'
|
||||
}
|
||||
}))
|
||||
}
|
||||
messages.push(toAdd)
|
||||
addedMessages.push(toAdd)
|
||||
for (const toolCall of toolCalls) {
|
||||
const messageToAdd = await processToolCall({
|
||||
tools,
|
||||
toolCall,
|
||||
helpers,
|
||||
toolCallbacks: callbacks
|
||||
})
|
||||
messages.push(messageToAdd)
|
||||
addedMessages.push(messageToAdd)
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function getResponseFromEvent(part: OpenAI.Chat.Completions.ChatCompletionChunk): string {
|
||||
return part.choices?.[0]?.delta?.content || ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user