From d4ac089a9a0091922a5e065263bd6d16e6b8fc7e Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Wed, 18 Jun 2025 20:32:04 +0800 Subject: [PATCH] support xml --- package-lock.json | 42 ++++- renderer/package.json | 4 +- .../main-panel/chat/chat-box/chat.ts | 8 +- .../chat/chat-box/options/tool-use.vue | 43 ++--- .../main-panel/chat/core/handle-tool-calls.ts | 48 +++++- .../main-panel/chat/core/task-loop.ts | 160 +++++++++++++++--- .../chat/core/{prompt.ts => xml-wrapper.ts} | 135 +++++++++++++-- 7 files changed, 366 insertions(+), 74 deletions(-) rename renderer/src/components/main-panel/chat/core/{prompt.ts => xml-wrapper.ts} (58%) diff --git a/package-lock.json b/package-lock.json index a59a012..e6f218c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2698,6 +2698,16 @@ "@types/node": "*" } }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", @@ -9401,6 +9411,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", @@ -11392,6 +11408,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -11579,7 +11617,8 @@ "uuid": "^11.1.0", "vue": "^3.5.13", "vue-i18n": "^11.1.0", - "vue-router": "^4.5.0" + "vue-router": "^4.5.0", + "xml2js": "^0.6.2" }, "devDependencies": { "@babel/core": "^7.27.1", @@ -11593,6 +11632,7 @@ "@types/markdown-it": "^14.1.2", "@types/node": "^22.14.0", "@types/prismjs": "^1.26.5", + "@types/xml2js": "^0.4.14", "@vitejs/plugin-vue": "^5.2.3", "@vue/babel-plugin-jsx": "^1.4.0", "@vue/devtools-core": "^7.7.6", diff --git a/renderer/package.json b/renderer/package.json index c0c3ad5..40f3604 100644 --- a/renderer/package.json +++ b/renderer/package.json @@ -33,7 +33,8 @@ "uuid": "^11.1.0", "vue": "^3.5.13", "vue-i18n": "^11.1.0", - "vue-router": "^4.5.0" + "vue-router": "^4.5.0", + "xml2js": "^0.6.2" }, "devDependencies": { "@babel/core": "^7.27.1", @@ -47,6 +48,7 @@ "@types/markdown-it": "^14.1.2", "@types/node": "^22.14.0", "@types/prismjs": "^1.26.5", + "@types/xml2js": "^0.4.14", "@vitejs/plugin-vue": "^5.2.3", "@vue/babel-plugin-jsx": "^1.4.0", "@vue/devtools-core": "^7.7.6", 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 662e659..0b2d003 100644 --- a/renderer/src/components/main-panel/chat/chat-box/chat.ts +++ b/renderer/src/components/main-panel/chat/chat-box/chat.ts @@ -1,4 +1,4 @@ -import type { ToolCallContent, ToolItem } from "@/hook/type"; +import type { InputSchema, ToolCallContent, ToolItem } from "@/hook/type"; import { type Ref, ref } from "vue"; import type { OpenAI } from 'openai'; @@ -16,6 +16,7 @@ export enum MessageState { Success = 'success', ParseJsonError = 'parse json error', NoToolFunction = 'no tool function', + InvalidXml = 'invalid xml', } export interface IExtraInfo { @@ -23,6 +24,7 @@ export interface IExtraInfo { state: MessageState, serverName: string, usage?: ChatCompletionChunk['usage']; + enableXmlWrapper: boolean; [key: string]: any; } @@ -52,7 +54,7 @@ export interface EnableToolItem { name: string; description: string; enabled: boolean; - inputSchema: any; + inputSchema: InputSchema; } export interface ChatSetting { @@ -108,7 +110,7 @@ export interface IToolRenderMessage { export type IRenderMessage = ICommonRenderMessage | IToolRenderMessage; -export function getToolSchema(enableTools: EnableToolItem[]) { +export function getToolSchema(enableTools: EnableToolItem[]): any[] { const toolsSchema = []; for (let i = 0; i < enableTools.length; i++) { const enableTool = enableTools[i]; diff --git a/renderer/src/components/main-panel/chat/chat-box/options/tool-use.vue b/renderer/src/components/main-panel/chat/chat-box/options/tool-use.vue index d877a41..8a03c59 100644 --- a/renderer/src/components/main-panel/chat/chat-box/options/tool-use.vue +++ b/renderer/src/components/main-panel/chat/chat-box/options/tool-use.vue @@ -16,11 +16,9 @@
{{ t('tool-manage') }} - xml + }" @click="tabStorage.settings.enableXmlWrapper = !tabStorage.settings.enableXmlWrapper">xml
@@ -57,7 +55,7 @@ import { useI18n } from 'vue-i18n'; import { type ChatStorage, type EnableToolItem, getToolSchema } from '../chat'; import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown'; import { mcpClientAdapter } from '@/views/connect/core'; -import { toolSchemaToPromptDescription } from '../../core/prompt'; +import { toolSchemaToPromptDescription } from '../../core/xml-wrapper'; const { t } = useI18n(); @@ -66,37 +64,34 @@ const tabStorage = inject('tabStorage') as ChatStorage; const showToolsDialog = ref(false); const availableToolsNum = computed(() => { - return tabStorage.settings.enableTools.filter(tool => tool.enabled).length; + return tabStorage.settings.enableTools.filter(tool => tool.enabled).length; }); // 修改 toggleTools 方法 const toggleTools = () => { - showToolsDialog.value = true; + showToolsDialog.value = true; }; -const activeToolsSchemaHTML = computed(() => { - const toolsSchema = getToolSchema(tabStorage.settings.enableTools); - const jsonString = JSON.stringify(toolsSchema, null, 2); - return markdownToHtml( - "```json\n" + jsonString + "\n```" - ); +const activeToolsSchemaHTML = computed(() => { + const toolsSchema = getToolSchema(tabStorage.settings.enableTools); + const jsonString = JSON.stringify(toolsSchema, null, 2); + + return markdownToHtml( + "```json\n" + jsonString + "\n```" + ); }); -const activeToolsXmlPrompt = computed(() => { - if (tabStorage.settings.enableXmlWrapper) { - const prompt = toolSchemaToPromptDescription(tabStorage.settings.enableTools); - return markdownToHtml( - "```markdown\n" + prompt + "\n```" - ); - } else { - return ''; - } +const activeToolsXmlPrompt = computed(() => { + const prompt = toolSchemaToPromptDescription(tabStorage.settings.enableTools); + return markdownToHtml( + "```markdown\n" + prompt + "\n```" + ); }); // 新增方法 - 激活所有工具 const enableAllTools = () => { - tabStorage.settings.enableTools.forEach(tool => { + tabStorage.settings.enableTools.forEach(tool => { tool.enabled = true; }); }; @@ -151,7 +146,6 @@ onMounted(async () => { \ No newline at end of file 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 index 75844fd..7ae62ad 100644 --- a/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts +++ b/renderer/src/components/main-panel/chat/core/handle-tool-calls.ts @@ -2,7 +2,14 @@ import type { ToolCallContent, ToolCallResponse } from "@/hook/type"; import { MessageState, type ToolCall } from "../chat-box/chat"; import { mcpClientAdapter } from "@/views/connect/core"; import type { BasicLlmDescription } from "@/views/setting/llm"; -import { redLog } from "@/views/setting/util"; +import type OpenAI from "openai"; + +export interface TaskLoopChatOption { + id?: string + proxyServer?: string + enableXmlWrapper?: boolean +} +export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & TaskLoopChatOption; export interface ToolCallResult { state: MessageState; @@ -60,7 +67,7 @@ function deserializeToolCallResponse(toolArgs: string) { } } -function handleToolResponse(toolResponse: ToolCallResponse) { +export function handleToolResponse(toolResponse: ToolCallResponse) { if (typeof toolResponse === 'string') { return { @@ -98,7 +105,14 @@ function parseErrorObject(error: any): string { } } -function grokIndexAdapter(toolCall: ToolCall, callId2Index: Map): IToolCallIndex { + +/** + * @description 将工具调用的ID映射为索引 + * @param toolCall 工具调用对象 + * @param callId2Index ID到索引的映射表 + * @returns 映射后的索引值 + */ +export function idAsIndexAdapter(toolCall: ToolCall, callId2Index: Map): IToolCallIndex { // grok 采用 id 作为 index,需要将 id 映射到 zero-based 的 index if (!toolCall.id) { return 0; @@ -109,24 +123,42 @@ function grokIndexAdapter(toolCall: ToolCall, callId2Index: Map) return callId2Index.get(toolCall.id)!; } -function geminiIndexAdapter(toolCall: ToolCall): IToolCallIndex { + +/** + * @description 单次调用的索引适配器(暂未实现) + * @param toolCall 工具调用对象 + * @returns 固定返回0 + */ +export function singleCallIndexAdapter(toolCall: ToolCall): IToolCallIndex { // TODO: 等待后续支持 return 0; } -function defaultIndexAdapter(toolCall: ToolCall): IToolCallIndex { +/** + * @description + * @param toolCall + * @returns + */ +export function defaultIndexAdapter(toolCall: ToolCall): IToolCallIndex { return toolCall.index || 0; } -export function getToolCallIndexAdapter(llm: BasicLlmDescription) { +export function getToolCallIndexAdapter(llm: BasicLlmDescription, chatData: ChatCompletionCreateParamsBase) { + + // 如果是 xml 模式,那么 index adapter 必须是 idAsIndexAdapter + + if (chatData.enableXmlWrapper) { + const callId2Index = new Map(); + return (toolCall: ToolCall) => idAsIndexAdapter(toolCall, callId2Index); + } if (llm.userModel.startsWith('gemini')) { - return geminiIndexAdapter; + return singleCallIndexAdapter; } if (llm.userModel.startsWith('grok')) { const callId2Index = new Map(); - return (toolCall: ToolCall) => grokIndexAdapter(toolCall, callId2Index); + return (toolCall: ToolCall) => idAsIndexAdapter(toolCall, callId2Index); } return defaultIndexAdapter; 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 e9f058c..2a051dd 100644 --- a/renderer/src/components/main-panel/chat/core/task-loop.ts +++ b/renderer/src/components/main-panel/chat/core/task-loop.ts @@ -1,6 +1,6 @@ /* eslint-disable */ import { ref, type Ref } from "vue"; -import { type ToolCall, type ChatStorage, getToolSchema, MessageState } from "../chat-box/chat"; +import { type ToolCall, type ChatStorage, getToolSchema, MessageState, type ChatMessage } from "../chat-box/chat"; import { useMessageBridge, MessageBridge, createMessageBridge } from "@/api/message-bridge"; import type { OpenAI } from 'openai'; import { llmManager, llms, type BasicLlmDescription } from "@/views/setting/llm"; @@ -13,6 +13,7 @@ import { mcpSetting } from "@/hook/mcp"; import { mcpClientAdapter } from "@/views/connect/core"; import type { ToolItem } from "@/hook/type"; import chalk from 'chalk'; +import { getXmlWrapperPrompt, getToolCallFromXmlString, getXmlsFromString, handleXmlWrapperToolcall, toNormaliseToolcall, getXmlResultPrompt } from "./xml-wrapper"; export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; export interface TaskLoopChatOption { @@ -90,14 +91,26 @@ export class TaskLoop { this.bridge = useMessageBridge(); } - private handleChunkDeltaContent(chunk: ChatCompletionChunk) { + /** + * @description 处理 streaming 输出的每一个分块的 content 部分 + * 值得一提的是,如果开启了 xml 指令包裹,那么 toocall 模块部分也由此处来完成 + * @param chunk + * @param chatData + */ + private handleChunkDeltaContent(chunk: ChatCompletionChunk, chatData: ChatCompletionCreateParamsBase) { const content = chunk.choices[0]?.delta?.content || ''; if (content) { this.streamingContent.value += content; } } - private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex) { + /** + * @description 处理 streaming 输出的每一个 chunk 的 tool_calls 部分 + * @param chunk + * @param chatData + * @param toolcallIndexAdapter + */ + private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk, chatData: ChatCompletionCreateParamsBase, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex) { const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0]; if (toolCall) { @@ -154,12 +167,12 @@ export class TaskLoop { // 处理增量的 content 和 tool_calls if (chatData.enableXmlWrapper) { - this.handleChunkDeltaContent(chunk); + this.handleChunkDeltaContent(chunk, chatData); // no tool call in enableXmlWrapper this.handleChunkUsage(chunk); } else { - this.handleChunkDeltaContent(chunk); - this.handleChunkDeltaToolCalls(chunk, toolcallIndexAdapter); + this.handleChunkDeltaContent(chunk, chatData); + this.handleChunkDeltaToolCalls(chunk, chatData, toolcallIndexAdapter); this.handleChunkUsage(chunk); } @@ -218,24 +231,35 @@ export class TaskLoop { const model = this.getLlmConfig().userModel; const temperature = tabStorage.settings.temperature; - const tools = getToolSchema(tabStorage.settings.enableTools); const parallelToolCalls = tabStorage.settings.parallelToolCalls; const proxyServer = mcpSetting.proxyServer || ''; + + // 如果是 xml 模式,则 tools 为空 const enableXmlWrapper = tabStorage.settings.enableXmlWrapper; + const tools = enableXmlWrapper ? []: getToolSchema(tabStorage.settings.enableTools); const userMessages = []; // 尝试获取 system prompt,在 api 模式下,systemPrompt 就是目标提词 // 但是在 UI 模式下,systemPrompt 只是一个 index,需要从后端数据库中获取真实 prompt - if (tabStorage.settings.systemPrompt) { - const prompt = getSystemPrompt(tabStorage.settings.systemPrompt) || tabStorage.settings.systemPrompt; - userMessages.push({ - role: 'system', - content: prompt - }); + let prompt = ''; + + // 如果存在系统提示词,则从数据库中获取对应的数据 + if (tabStorage.settings.systemPrompt) { + prompt += getSystemPrompt(tabStorage.settings.systemPrompt) || tabStorage.settings.systemPrompt; } + // 如果是 xml 模式,则在开头注入 xml + if (enableXmlWrapper) { + prompt += getXmlWrapperPrompt(tabStorage.settings.enableTools, tabStorage); + } + + userMessages.push({ + role: 'system', + content: prompt + }); + // 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息 const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength); userMessages.push(...loadMessages); @@ -253,7 +277,7 @@ export class TaskLoop { parallelToolCalls, messages: userMessages, proxyServer, - enableXmlWrapper + enableXmlWrapper, } as ChatCompletionCreateParamsBase; return chatData; @@ -474,6 +498,7 @@ export class TaskLoop { // 等待连接完成 await this.nodejsStatus.connectionFut; } + const enableXmlWrapper = tabStorage.settings.enableXmlWrapper; // 添加目前的消息 tabStorage.messages.push({ @@ -482,7 +507,8 @@ export class TaskLoop { extraInfo: { created: Date.now(), state: MessageState.Success, - serverName: this.getLlmConfig().id || 'unknown' + serverName: this.getLlmConfig().id || 'unknown', + enableXmlWrapper } }); @@ -511,7 +537,7 @@ export class TaskLoop { this.currentChatId = chatData.id!; const llm = this.getLlmConfig(); - const toolcallIndexAdapter = getToolCallIndexAdapter(llm); + const toolcallIndexAdapter = getToolCallIndexAdapter(llm, chatData); // 发送请求 const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter); @@ -526,7 +552,8 @@ export class TaskLoop { extraInfo: { created: Date.now(), state: MessageState.Success, - serverName: this.getLlmConfig().id || 'unknown' + serverName: this.getLlmConfig().id || 'unknown', + enableXmlWrapper } }); @@ -563,7 +590,8 @@ export class TaskLoop { created: Date.now(), state: toolCallResult.state, serverName: this.getLlmConfig().id || 'unknown', - usage: undefined + usage: undefined, + enableXmlWrapper } }); break; @@ -578,7 +606,8 @@ export class TaskLoop { created: Date.now(), state: toolCallResult.state, serverName: this.getLlmConfig().id || 'unknown', - usage: this.completionUsage + usage: this.completionUsage, + enableXmlWrapper } }); } else if (toolCallResult.state === MessageState.ToolCall) { @@ -592,7 +621,8 @@ export class TaskLoop { created: Date.now(), state: toolCallResult.state, serverName: this.getLlmConfig().id || 'unknown', - usage: this.completionUsage + usage: this.completionUsage, + enableXmlWrapper } }); } @@ -606,10 +636,96 @@ export class TaskLoop { created: Date.now(), state: MessageState.Success, serverName: this.getLlmConfig().id || 'unknown', - usage: this.completionUsage + usage: this.completionUsage, + enableXmlWrapper } }); - break; + + // 如果 xml 模型,需要检查内部是否含有有效的 xml 进行调用 + if (tabStorage.settings.enableXmlWrapper) { + const xmls = getXmlsFromString(this.streamingContent.value); + if (xmls.length === 0) { + // 没有 xml 了,说明对话结束 + break; + } + + // 使用 user 作为身份来承载 xml 调用的结果 + // 并且在 extra 内存储结构化信息 + const fakeUserMessage = { + role: 'user', + content: '', + extraInfo: { + created: Date.now(), + state: MessageState.Success, + serverName: this.getLlmConfig().id || 'unknown', + usage: this.completionUsage, + enableXmlWrapper, + } + } as ChatMessage; + + // 有 xml 了,需要检查 xml 内部是否有有效的 xml 进行调用 + for (const xml of xmls) { + const toolcall = await getToolCallFromXmlString(xml); + + if (!toolcall) { + continue; + } + + // toolcall 事件 + // 此处使用的是 xml 使用的 toolcall,为了保持一致性,需要转换成 openai 标准下的 toolcall + const normaliseToolcall = toNormaliseToolcall(toolcall, toolcallIndexAdapter); + this.consumeToolCalls(normaliseToolcall); + + // 调用 XML 调用,其实可以考虑后续把这个循环改成 Promise.race + const toolCallResult = await handleXmlWrapperToolcall(toolcall); + + // toolcalled 事件 + // 因为是交付给后续进行统一消费的,所以此处的输出满足 openai 接口规范 + this.consumeToolCalleds(toolCallResult); + + // XML 模式下只存在 assistant 和 user 这两个角色,因此,以 user 为身份来存储 + if (toolCallResult.state === MessageState.InvalidXml) { + // 如果是因为解析 XML 错误,则重新开始 + tabStorage.messages.pop(); + jsonParseErrorRetryCount ++; + + redLog('解析 XML 错误 ' + normaliseToolcall?.function?.arguments); + + // 如果因为 XML 错误而失败太多,就只能中断了 + if (jsonParseErrorRetryCount >= (this.taskOptions.maxJsonParseRetry || 3)) { + + const prompt = getXmlResultPrompt(toolcall.callId, `解析 XML 错误,无法继续调用工具 (累计错误次数 ${this.taskOptions.maxJsonParseRetry})`); + + fakeUserMessage.content += prompt; + + break; + } + } else if (toolCallResult.state === MessageState.Success) { + // TODO: xml 目前只支持 text 类型的回复 + const toolCallResultString = toolCallResult.content + .filter(c => c.type === 'text') + .map(c => c.text) + .join('\n'); + + fakeUserMessage.content += getXmlResultPrompt(toolcall.callId, toolCallResultString); + + } else if (toolCallResult.state === MessageState.ToolCall) { + // TODO: xml 目前只支持 text 类型的回复 + const toolCallResultString = toolCallResult.content + .filter(c => c.type === 'text') + .map(c => c.text) + .join('\n'); + + fakeUserMessage.content += getXmlResultPrompt(toolcall.callId, toolCallResultString); + } + } + + tabStorage.messages.push(fakeUserMessage); + + } else { + // 普通对话直接结束 + break; + } } else { // 一些提示 diff --git a/renderer/src/components/main-panel/chat/core/prompt.ts b/renderer/src/components/main-panel/chat/core/xml-wrapper.ts similarity index 58% rename from renderer/src/components/main-panel/chat/core/prompt.ts rename to renderer/src/components/main-panel/chat/core/xml-wrapper.ts index e66a8b9..fee8717 100644 --- a/renderer/src/components/main-panel/chat/core/prompt.ts +++ b/renderer/src/components/main-panel/chat/core/xml-wrapper.ts @@ -1,7 +1,21 @@ -import type { ToolItem } from "@/hook/type"; +import { parseString } from 'xml2js'; +import { MessageState, type ToolCall } from '../chat-box/chat'; +import { mcpClientAdapter } from '@/views/connect/core'; +import { handleToolResponse, type IToolCallIndex, type ToolCallResult } from './handle-tool-calls'; +import type { ChatStorage, EnableToolItem } from "../chat-box/chat"; -export function toolSchemaToPromptDescription(tools: ToolItem[]) { +export interface XmlToolCall { + server: string; + name: string; + callId: string; + parameters: Record; +} + + +export function toolSchemaToPromptDescription(enableTools: EnableToolItem[]) { let prompt = ''; + + const tools = enableTools.filter(tool => tool.enabled); // 无参数的工具 const noParamTools = tools.filter(tool => @@ -38,15 +52,32 @@ export function toolSchemaToPromptDescription(tools: ToolItem[]) { return prompt; } -export function getXmlWrapperPrompt(tools: ToolItem[]) { +export function getXmlWrapperPrompt(tools: EnableToolItem[], tabStorage: ChatStorage) { const toolPrompt = toolSchemaToPromptDescription(tools); + const requests = [ + `ALWAYS analyze what function calls would be appropriate for the task`, + `ALWAYS format your function call usage EXACTLY as specified in the schema`, + `NEVER skip required parameters in function calls`, + `NEVER invent functions that arent available to you`, + `ALWAYS wait for function call execution results before continuing`, + `After invoking a function, wait for the output in tag and then continue with your response`, + `NEVER mock or form on your own, it will be provided to you after the execution`, + ]; + + if (!tabStorage.settings.parallelToolCalls) { + requests.push(`NEVER invoke multiple functions in a single response`); + } + + const requestString = requests.map((text, index) => { + return `${index + 1}. ${text}`; + }).join('\n'); return ` [Start Fresh Session from here] -You are SuperAssistant with the capabilities of invoke functions and make the best use of it during your assistance, a knowledgeable assistant focused on answering questions and providing information on any topics. +You are OpenMCP Assistant with the capabilities of invoke functions and make the best use of it during your assistance, a knowledgeable assistant focused on answering questions and providing information on any topics. In this environment you have access to a set of tools you can use to answer the user's question. You have access to a set of functions you can use to answer the user's question. You do NOT currently have the ability to inspect files or interact with external resources, except by invoking the below functions. @@ -94,15 +125,7 @@ You can invoke one or more functions by writing a "" block like String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions. When a user makes a request: -1. ALWAYS analyze what function calls would be appropriate for the task -2. ALWAYS format your function call usage EXACTLY as specified in the schema -3. NEVER skip required parameters in function calls -4. NEVER invent functions that arent available to you -5. ALWAYS wait for function call execution results before continuing -6. After invoking a function, wait for the output in tag and then continue with your response -7. NEVER invoke multiple functions in a single response -8. NEVER mock or form on your own, it will be provided to you after the execution - +${requestString} Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted. @@ -148,6 +171,90 @@ User Interaction Starts here: } -export function getXmlWrapperPromptCn() { +export function getXmlResultPrompt(callId: string, result: string) { + return ` +\`\`\`xml + + +${result} + + +\`\`\` + `.trim() + '\n\n'; +} +export function getXmlsFromString(content: string) { + const matches = content.matchAll(/```xml\n([\s\S]*?)\n```/g); + return Array.from(matches).map(match => match[1].trim()); +} + + +export async function getToolCallFromXmlString(xmlString: string): Promise { + try { + const result = await new Promise((resolve, reject) => { + parseString(xmlString, (err, result) => { + if (err) reject(err); + else resolve(result); + }); + }); + + if (!result?.function_calls?.invoke) { + return null; + } + + const invoke = result.function_calls.invoke[0].$; + const parameters: Record = {}; + + if (result.function_calls.invoke[0].parameter) { + result.function_calls.invoke[0].parameter.forEach((param: any) => { + const name = param.$.name as string; + parameters[name] = param._; + }); + } + + // name 可能是 neo4j-mcp.executeReadOnlyCypherQuery + return { + server: '', + name: invoke.name, + callId: invoke.call_id, + parameters + }; + } catch (error) { + console.error('Failed to parse function calls:', error); + return null; + } +} + +export function toNormaliseToolcall(xmlToolcall: XmlToolCall, toolcallIndexAdapter: (toolCall: ToolCall) => IToolCallIndex): ToolCall { + const toolcall = { + id: xmlToolcall.callId, + index: -1, + type: 'function', + function: { + name: xmlToolcall.name, + arguments: JSON.stringify(xmlToolcall.parameters) + } + } as ToolCall; + + toolcall.index = toolcallIndexAdapter(toolcall); + + return toolcall; +} + +export async function handleXmlWrapperToolcall(toolcall: XmlToolCall): Promise { + if (!toolcall) { + return { + content: [{ + type: 'error', + text: 'invalid xml' + }], + state: MessageState.InvalidXml + } + } + + // 进行调用,根据结果返回不同的值 + console.log(toolcall); + + const toolResponse = await mcpClientAdapter.callTool(toolcall.name, toolcall.parameters); + return handleToolResponse(toolResponse); } \ No newline at end of file