From 4c0566b4706663ae7857088fc68c8132698b0a10 Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Thu, 8 May 2025 22:28:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=80=E6=AC=A1=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=90=8C=E6=97=B6=E8=B0=83=E7=94=A8=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- renderer/public/mcp.css | 4 + .../main-panel/chat/chat-box/chat.ts | 18 +- .../main-panel/chat/chat-box/index.vue | 26 ++- .../main-panel/chat/core/handle-tool-calls.ts | 81 ++++++++ .../main-panel/chat/core/task-loop.ts | 178 +++++------------- .../src/components/main-panel/chat/index.vue | 15 +- .../main-panel/chat/message/toolcall.vue | 176 ++++++++++------- .../src/components/main-panel/tool/tools.ts | 3 - 9 files changed, 289 insertions(+), 214 deletions(-) create mode 100644 renderer/src/components/main-panel/chat/core/handle-tool-calls.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 183abd8..4486c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,5 @@ # Change Log - ## [main] 0.0.8 - 大模型 API 测试时更加完整的报错 - 修复 0.0.7 引入的bug:修改对话无法发出 @@ -8,6 +7,7 @@ - 修复 bug:富文本编辑器发送前缀为空的字符会全部为空 - 修复 bug:流式传输进行 function calling 时,多工具的索引串流导致的 JSON Schema 反序列化失败 - 修复 bug:大模型返回大量重复错误信息 +- 新特性:支持一次对话同时调用多个工具 ## [main] 0.0.7 - 优化页面布局,使得调试窗口可以显示更多内容 diff --git a/renderer/public/mcp.css b/renderer/public/mcp.css index 38374ab..2636e20 100644 --- a/renderer/public/mcp.css +++ b/renderer/public/mcp.css @@ -131,6 +131,10 @@ a { height: 5px; } +.tool-arguments .openmcp-code-block pre code::-webkit-scrollbar { + background: transparent !important; +} + .tool-arguments .openmcp-code-block pre code { background: transparent !important; } diff --git a/renderer/src/components/main-panel/chat/chat-box/chat.ts b/renderer/src/components/main-panel/chat/chat-box/chat.ts index a3b9f77..a295e04 100644 --- a/renderer/src/components/main-panel/chat/chat-box/chat.ts +++ b/renderer/src/components/main-panel/chat/chat-box/chat.ts @@ -27,6 +27,7 @@ export interface IExtraInfo { export interface ToolMessage { role: 'tool'; + index: number; content: ToolCallContent[]; tool_call_id?: string name?: string // 工具名称,当 role 为 tool @@ -95,15 +96,24 @@ export type RichTextItem = PromptTextItem | ResourceTextItem | TextItem; export const allTools = ref([]); -export interface IRenderMessage { - role: 'user' | 'assistant/content' | 'assistant/tool_calls' | 'tool'; +export interface ICommonRenderMessage { + role: 'user' | 'assistant/content'; content: string; - toolResult?: ToolCallContent[]; - tool_calls?: ToolCall[]; showJson?: Ref; extraInfo: IExtraInfo; } +export interface IToolRenderMessage { + role: 'assistant/tool_calls'; + content: string; + toolResults: ToolCallContent[][]; + tool_calls: ToolCall[]; + showJson?: Ref; + extraInfo: IExtraInfo; +} + +export type IRenderMessage = ICommonRenderMessage | IToolRenderMessage; + export function getToolSchema(enableTools: EnableToolItem[]) { const toolsSchema = []; for (let i = 0; i < enableTools.length; i++) { diff --git a/renderer/src/components/main-panel/chat/chat-box/index.vue b/renderer/src/components/main-panel/chat/chat-box/index.vue index ce4c1af..a5d7f13 100644 --- a/renderer/src/components/main-panel/chat/chat-box/index.vue +++ b/renderer/src/components/main-panel/chat/chat-box/index.vue @@ -65,6 +65,23 @@ const chatContext = inject('chatContext') as any; chatContext.handleSend = handleSend; +function clearErrorMessage(errorMessage: string) { + try { + const errorObject = JSON.parse(errorMessage); + if (errorObject.error) { + return errorObject.error; + } + if (errorObject.message) { + return errorObject.message; + } + if (errorObject.msg) { + return errorObject.msg; + } + } catch (error) { + return errorMessage; + } +} + function handleSend(newMessage?: string) { // 将富文本信息转换成纯文本信息 const userMessage = newMessage || userInput.value; @@ -79,13 +96,16 @@ function handleSend(newMessage?: string) { loop = new TaskLoop(streamingContent, streamingToolCalls); loop.registerOnError((error) => { - - ElMessage.error(error.msg); + console.log('error.msg'); + console.log(error.msg); + + const errorMessage = clearErrorMessage(error.msg); + ElMessage.error(errorMessage); if (error.state === MessageState.ReceiveChunkError) { tabStorage.messages.push({ role: 'assistant', - content: error.msg, + content: errorMessage, extraInfo: { created: Date.now(), state: error.state, diff --git a/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts b/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts new file mode 100644 index 0000000..b185055 --- /dev/null +++ b/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts @@ -0,0 +1,81 @@ +import { ToolCallResponse } from "@/hook/type"; +import { callTool } from "../../tool/tools"; +import { MessageState, ToolCall } from "../chat-box/chat"; + +export async function handleToolCalls(toolCall: ToolCall) { + // 反序列化 streaming 来的参数字符串 + const toolName = toolCall.function.name; + const argsResult = deserializeToolCallResponse(toolCall.function.arguments); + + if (argsResult.error) { + return { + content: [{ + type: 'error', + text: parseErrorObject(argsResult.error) + }], + state: MessageState.ParseJsonError + }; + } + + const toolArgs = argsResult.value; + + // 进行调用,根据结果返回不同的值 + const toolResponse = await callTool(toolName, toolArgs); + return handleToolResponse(toolResponse); +} + +function deserializeToolCallResponse(toolArgs: string) { + try { + const args = JSON.parse(toolArgs); + return { + value: args, + error: undefined + } + } catch (error) { + return { + value: undefined, + error + } + } +} + +function handleToolResponse(toolResponse: ToolCallResponse) { + if (typeof toolResponse === 'string') { + // 如果是 string,说明是错误信息 + console.log(toolResponse); + + + return { + content: [{ + type: 'error', + text: toolResponse + }], + state: MessageState.ToolCall + } + + } else if (!toolResponse.isError) { + + return { + content: toolResponse.content, + state: MessageState.Success + }; + + } else { + + return { + content: toolResponse.content, + state: MessageState.ToolCall + }; + + } +} + +function parseErrorObject(error: any): string { + if (typeof error === 'string') { + return error; + } else if (typeof error === 'object') { + return JSON.stringify(error, null, 2); + } else { + return error.toString(); + } +} \ No newline at end of file diff --git a/renderer/src/components/main-panel/chat/core/task-loop.ts b/renderer/src/components/main-panel/chat/core/task-loop.ts index 0d4138c..818856f 100644 --- a/renderer/src/components/main-panel/chat/core/task-loop.ts +++ b/renderer/src/components/main-panel/chat/core/task-loop.ts @@ -7,6 +7,7 @@ import { callTool } from "../../tool/tools"; import { llmManager, llms } from "@/views/setting/llm"; import { pinkLog, redLog } from "@/views/setting/util"; import { ElMessage } from "element-plus"; +import { handleToolCalls } from "./handle-tool-calls"; export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string }; @@ -44,87 +45,6 @@ export class TaskLoop { } - private async handleToolCalls(toolCalls: ToolCall[]) { - // TODO: 调用多个工具并返回调用结果? - - const toolCall = toolCalls[0]; - - console.log('debug toolcall'); - console.log(toolCalls); - - let toolName: string; - let toolArgs: Record; - - try { - toolName = toolCall.function.name; - toolArgs = JSON.parse(toolCall.function.arguments); - } catch (error) { - return { - content: [{ - type: 'error', - text: this.parseErrorObject(error) - }], - state: MessageState.ParseJsonError - }; - } - - - try { - const toolResponse = await callTool(toolName, toolArgs); - - console.log(toolResponse); - - if (typeof toolResponse === 'string') { - console.log(toolResponse); - - return { - content: [{ - type: 'error', - text: toolResponse - }], - state: MessageState.ToolCall - } - } else if (!toolResponse.isError) { - - return { - content: toolResponse.content, - state: MessageState.Success - }; - } else { - - return { - content: toolResponse.content, - state: MessageState.ToolCall - } - } - - } catch (error) { - this.onError({ - state: MessageState.ToolCall, - msg: `工具调用失败: ${(error as Error).message}` - }); - console.error(error); - - return { - content: [{ - type: 'error', - text: this.parseErrorObject(error) - }], - state: MessageState.ToolCall - } - } - } - - private parseErrorObject(error: any): string { - if (typeof error === 'string') { - return error; - } else if (typeof error === 'object') { - return JSON.stringify(error, null, 2); - } else { - return error.toString(); - } - } - private handleChunkDeltaContent(chunk: ChatCompletionChunk) { const content = chunk.choices[0]?.delta?.content || ''; if (content) { @@ -199,12 +119,7 @@ export class TaskLoop { }); }, { once: true }); - console.log('register error handler'); - const errorHandler = this.bridge.addCommandListener('llm/chat/completions/error', data => { - - console.log('enter error report'); - this.onError({ state: MessageState.ReceiveChunkError, msg: data.msg || '请求模型服务时发生错误' @@ -219,6 +134,8 @@ export class TaskLoop { }, { once: true }); + console.log(chatData); + this.bridge.postMessage({ command: 'llm/chat/completions', data: JSON.parse(JSON.stringify(chatData)), @@ -360,59 +277,58 @@ export class TaskLoop { pinkLog('调用工具数量:' + this.streamingToolCalls.value.length); - const toolCallResult = await this.handleToolCalls(this.streamingToolCalls.value); - - console.log('toolCallResult', toolCallResult); - - if (toolCallResult.state === MessageState.ParseJsonError) { - // 如果是因为解析 JSON 错误,则重新开始 - tabStorage.messages.pop(); - jsonParseErrorRetryCount ++; - - redLog('解析 JSON 错误 ' + this.streamingToolCalls.value[0]?.function?.arguments); - - // 如果因为 JSON 错误而失败太多,就只能中断了 - if (jsonParseErrorRetryCount >= this.taskOptions.maxJsonParseRetry) { + for (const toolCall of this.streamingToolCalls.value || []) { + const toolCallResult = await handleToolCalls(toolCall); + + if (toolCallResult.state === MessageState.ParseJsonError) { + // 如果是因为解析 JSON 错误,则重新开始 + tabStorage.messages.pop(); + jsonParseErrorRetryCount ++; + + redLog('解析 JSON 错误 ' + toolCall?.function?.arguments); + + // 如果因为 JSON 错误而失败太多,就只能中断了 + if (jsonParseErrorRetryCount >= this.taskOptions.maxJsonParseRetry) { + tabStorage.messages.push({ + role: 'assistant', + content: `解析 JSON 错误,无法继续调用工具 (累计错误次数 ${this.taskOptions.maxJsonParseRetry})`, + extraInfo: { + created: Date.now(), + state: toolCallResult.state, + serverName: llms[llmManager.currentModelIndex].id || 'unknown', + usage: undefined + } + }); + break; + } + } else if (toolCallResult.state === MessageState.Success) { tabStorage.messages.push({ - role: 'assistant', - content: `解析 JSON 错误,无法继续调用工具 (累计错误次数 ${this.taskOptions.maxJsonParseRetry})`, + role: 'tool', + index: toolCall.index || 0, + tool_call_id: toolCall.id || toolCall.function.name, + content: toolCallResult.content, extraInfo: { created: Date.now(), state: toolCallResult.state, serverName: llms[llmManager.currentModelIndex].id || 'unknown', - usage: undefined + usage: this.completionUsage + } + }); + } else if (toolCallResult.state === MessageState.ToolCall) { + + tabStorage.messages.push({ + role: 'tool', + index: toolCall.index || 0, + tool_call_id: toolCall.id || toolCall.function.name, + content: toolCallResult.content, + extraInfo: { + created: Date.now(), + state: toolCallResult.state, + serverName: llms[llmManager.currentModelIndex].id || 'unknown', + usage: this.completionUsage } }); - break; } - } else if (toolCallResult.state === MessageState.Success) { - const toolCall = this.streamingToolCalls.value[0]; - - tabStorage.messages.push({ - role: 'tool', - tool_call_id: toolCall.id || toolCall.function.name, - content: toolCallResult.content, - extraInfo: { - created: Date.now(), - state: toolCallResult.state, - serverName: llms[llmManager.currentModelIndex].id || 'unknown', - usage: this.completionUsage - } - }); - } else if (toolCallResult.state === MessageState.ToolCall) { - const toolCall = this.streamingToolCalls.value[0]; - - tabStorage.messages.push({ - role: 'tool', - tool_call_id: toolCall.id || toolCall.function.name, - content: toolCallResult.content, - extraInfo: { - created: Date.now(), - state: toolCallResult.state, - serverName: llms[llmManager.currentModelIndex].id || 'unknown', - usage: this.completionUsage - } - }); } } else if (this.streamingContent.value) { diff --git a/renderer/src/components/main-panel/chat/index.vue b/renderer/src/components/main-panel/chat/index.vue index 88bdd82..da5bfef 100644 --- a/renderer/src/components/main-panel/chat/index.vue +++ b/renderer/src/components/main-panel/chat/index.vue @@ -25,7 +25,7 @@
@@ -97,6 +97,7 @@ const renderMessages = computed(() => { messages.push({ role: 'assistant/tool_calls', content: message.content, + toolResults: Array(message.tool_calls.length).fill([]), tool_calls: message.tool_calls, showJson: ref(false), extraInfo: { @@ -116,8 +117,16 @@ const renderMessages = computed(() => { // 如果是工具,则合并进入 之前 assistant 一起渲染 const lastAssistantMessage = messages[messages.length - 1]; if (lastAssistantMessage.role === 'assistant/tool_calls') { - lastAssistantMessage.toolResult = message.content; - lastAssistantMessage.extraInfo.state = message.extraInfo.state; + lastAssistantMessage.toolResults[message.index] = message.content; + + if (lastAssistantMessage.extraInfo.state === MessageState.Unknown) { + lastAssistantMessage.extraInfo.state = message.extraInfo.state; + } else if (lastAssistantMessage.extraInfo.state === MessageState.Success + || message.extraInfo.state !== MessageState.Success + ) { + lastAssistantMessage.extraInfo.state = message.extraInfo.state; + } + lastAssistantMessage.extraInfo.usage = lastAssistantMessage.extraInfo.usage || message.extraInfo.usage; } } diff --git a/renderer/src/components/main-panel/chat/message/toolcall.vue b/renderer/src/components/main-panel/chat/message/toolcall.vue index c1a147f..0a4f8b1 100644 --- a/renderer/src/components/main-panel/chat/message/toolcall.vue +++ b/renderer/src/components/main-panel/chat/message/toolcall.vue @@ -1,6 +1,6 @@ -
-
-
-
+
+ +
+
+ + + + {{ props.message.tool_calls[toolIndex].function.name }} + + + +
+
+ +
+
+
+
+
+
+ -
+
- + + + + {{ t("response") }} + + - {{ isValid ? t("response") : t('error') }} - + {{ isValid(toolResult) ? t("response") : t('error') }} + {{ t('feedback') }} - + + +
-
+
-
+
-
- +
+
-
+
{{ error }}
@@ -91,17 +110,12 @@
- +
- +
@@ -110,13 +124,13 @@ @@ -250,9 +288,6 @@ function updateToolCallResultItem(value: any, index: number) { padding: 3px 10px; } -.tool-result-content .el-progress-bar__outer { -} - .tool-result-content .progress { border-radius: .5em; background-color: var(--el-fill-color-light) !important; @@ -269,7 +304,7 @@ function updateToolCallResultItem(value: any, index: number) { } .message-text.tool_calls.warning .tool-result { - background-color: rgba(230, 162, 60, 0.5); + background-color: rgba(230, 162, 60, 0.5); } .message-text.tool_calls.error { @@ -281,7 +316,7 @@ function updateToolCallResultItem(value: any, index: number) { } .message-text.tool_calls.error .tool-result { - background-color: rgba(245, 108, 108, 0.5); + background-color: rgba(245, 108, 108, 0.5); } @@ -295,6 +330,9 @@ function updateToolCallResultItem(value: any, index: number) { padding-bottom: 5px; } +.toolcall-item .tool-calls { + margin-top: 22px; +} .tool-call-item { margin-bottom: 10px; diff --git a/renderer/src/components/main-panel/tool/tools.ts b/renderer/src/components/main-panel/tool/tools.ts index a867075..02ec19a 100644 --- a/renderer/src/components/main-panel/tool/tools.ts +++ b/renderer/src/components/main-panel/tool/tools.ts @@ -29,9 +29,6 @@ export function callTool(toolName: string, toolArgs: Record) { resolve(data.msg); } }, { once: true }); - - pinkLog('callTool'); - console.log(toolArgs); bridge.postMessage({ command: 'tools/call',