Merge branch 'feature/parallel-tool-test' of https://github.com/LSTM-Kirigaya/openmcp-client into feature/parallel-tool-test

This commit is contained in:
锦恢 2025-07-12 03:45:17 +08:00
commit a45f65816e
4 changed files with 169 additions and 24 deletions

View File

@ -161,6 +161,125 @@ export function topoSortParallel(state: DiagramState): string[][] {
return result; return result;
} }
export async function makeParallelTest(
dataViews: Reactive<NodeDataView>[],
enableXmlWrapper: boolean,
prompt: string | null = null,
context: DiagramContext
) {
if (dataViews.length === 0) {
return;
}
// 设置所有节点状态为运行中
const createAt = Date.now();
dataViews.forEach(dataView => {
dataView.status = 'running';
dataView.createAt = createAt;
});
context.render();
try {
const loop = new TaskLoop({ maxEpochs: 1 });
// 构建所有工具的信息
const allTools = dataViews.map(dataView => ({
name: dataView.tool.name,
description: dataView.tool.description,
inputSchema: dataView.tool.inputSchema,
enabled: true
}));
// 构建测试提示词,包含所有工具
const toolNames = dataViews.map(dv => dv.tool.name).join(', ');
const usePrompt = (prompt || 'please call the tools {tool} to make some test').replace('{tool}', toolNames);
const chatStorage = {
messages: [],
settings: {
temperature: 0.6,
systemPrompt: '',
enableTools: allTools,
enableWebSearch: false,
contextLength: 5,
enableXmlWrapper,
parallelToolCalls: true // 开启并行工具调用
}
} as ChatStorage;
loop.setMaxEpochs(1);
// 记录工具调用信息,用于匹配工具调用结果
const toolCallMap: Map<string, Reactive<NodeDataView>> = new Map(); // toolCallId -> dataView
let completedToolsCount = 0;
loop.registerOnToolCall(toolCall => {
// 找到对应的dataView
const dataView = dataViews.find(dv => dv.tool.name === toolCall.function?.name);
if (dataView) {
dataView.function = toolCall.function;
dataView.llmTimecost = Date.now() - createAt;
context.render();
// 记录工具调用ID与dataView的映射
if (toolCall.id) {
toolCallMap.set(toolCall.id, dataView);
}
}
return toolCall;
});
loop.registerOnToolCalled(toolCalled => {
// 这里我们需要改变策略因为没有工具调用ID信息
// 对于并行调用,我们可以根据工具调用的顺序来匹配结果
// 简单的策略:找到第一个仍在运行状态的工具
const runningView = dataViews.find(dv => dv.status === 'running');
if (runningView) {
runningView.toolcallTimecost = Date.now() - createAt - (runningView.llmTimecost || 0);
if (toolCalled.state === MessageState.Success) {
runningView.status = 'success';
runningView.result = toolCalled.content;
} else {
runningView.status = 'error';
runningView.result = toolCalled.content;
}
completedToolsCount++;
context.render();
// 如果所有工具都完成了,终止循环
if (completedToolsCount >= dataViews.length) {
loop.abort();
}
}
return toolCalled;
});
loop.registerOnError(error => {
dataViews.forEach(dataView => {
if (dataView.status === 'running') {
dataView.status = 'error';
dataView.result = error;
}
});
context.render();
});
await loop.start(chatStorage, usePrompt);
} finally {
const finishAt = Date.now();
dataViews.forEach(dataView => {
dataView.finishAt = finishAt;
if (dataView.status === 'running') {
dataView.status = 'success';
}
});
context.render();
}
}
export async function makeNodeTest( export async function makeNodeTest(
dataView: Reactive<NodeDataView>, dataView: Reactive<NodeDataView>,
enableXmlWrapper: boolean, enableXmlWrapper: boolean,

View File

@ -37,6 +37,13 @@
color: enableXmlWrapper ? 'var(--main-color)' : undefined color: enableXmlWrapper ? 'var(--main-color)' : undefined
}">XML</span> }">XML</span>
</div> </div>
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<el-switch v-model="enableParallelTest" style="margin-right: 8px;" />
<span :style="{
opacity: enableParallelTest ? 1 : 0.7,
color: enableParallelTest ? 'var(--main-color)' : undefined
}">{{ t('parallel-test') }}</span>
</div>
<div style="text-align: right;"> <div style="text-align: right;">
<el-button size="small" @click="testFormVisible = false">{{ t("cancel") }}</el-button> <el-button size="small" @click="testFormVisible = false">{{ t("cancel") }}</el-button>
<el-button size="small" type="primary" @click="onTestConfirm"> <el-button size="small" type="primary" @click="onTestConfirm">
@ -70,7 +77,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, provide, ref } from 'vue'; import { computed, nextTick, provide, ref } from 'vue';
import Diagram from './diagram.vue'; import Diagram from './diagram.vue';
import { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram'; import { makeNodeTest, makeParallelTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -137,23 +144,42 @@ if (autoDetectDiagram) {
// //
const testFormVisible = ref(false); const testFormVisible = ref(false);
const enableXmlWrapper = ref(false); const enableXmlWrapper = ref(false);
const enableParallelTest = ref(false);
const testPrompt = ref('please call the tool {tool} to make some test'); const testPrompt = ref('please call the tool {tool} to make some test');
async function onTestConfirm() { async function onTestConfirm() {
testFormVisible.value = false; testFormVisible.value = false;
// enableXmlWrapper.value testPrompt.value
const state = context.state; const state = context.state;
tabStorage.autoDetectDiagram!.views = []; tabStorage.autoDetectDiagram!.views = [];
if (state) { if (state) {
const dispatches = topoSortParallel(state); if (enableParallelTest.value) {
for (const nodeIds of dispatches) { //
for (const id of nodeIds) { const allViews = Array.from(state.dataView.values());
const view = state.dataView.get(id); await makeParallelTest(allViews, enableXmlWrapper.value, testPrompt.value, context);
if (view) {
await makeNodeTest(view, enableXmlWrapper.value, testPrompt.value, context) //
allViews.forEach(view => {
tabStorage.autoDetectDiagram!.views!.push({
tool: view.tool,
status: view.status,
function: view.function,
result: view.result,
createAt: view.createAt,
finishAt: view.finishAt,
llmTimecost: view.llmTimecost,
toolcallTimecost: view.toolcallTimecost,
});
});
} else {
//
const dispatches = topoSortParallel(state);
for (const nodeIds of dispatches) {
for (const id of nodeIds) {
const view = state.dataView.get(id);
if (view) {
await makeNodeTest(view, enableXmlWrapper.value, testPrompt.value, context);
tabStorage.autoDetectDiagram!.views!.push({ tabStorage.autoDetectDiagram!.views!.push({
tool: view.tool, tool: view.tool,
status: view.status, status: view.status,
@ -164,16 +190,14 @@ async function onTestConfirm() {
llmTimecost: view.llmTimecost, llmTimecost: view.llmTimecost,
toolcallTimecost: view.toolcallTimecost, toolcallTimecost: view.toolcallTimecost,
}); });
context.render(); context.render();
} }
} }
} }
}
} else { } else {
ElMessage.error('error'); ElMessage.error('error');
} }
} }
const resetPopoverVisible = ref(false); const resetPopoverVisible = ref(false);

View File

@ -157,6 +157,7 @@
"feedback": "Feedback", "feedback": "Feedback",
"waiting-mcp-server": "Waiting for MCP server response", "waiting-mcp-server": "Waiting for MCP server response",
"parallel-tool-calls": "Allow model to call multiple tools in single response", "parallel-tool-calls": "Allow model to call multiple tools in single response",
"parallel-test": "Parallel Test",
"proxy-server": "Proxy Server", "proxy-server": "Proxy Server",
"update-model-list": "Update Model List", "update-model-list": "Update Model List",
"preset-env-sync.success": "Preset environment variables synced", "preset-env-sync.success": "Preset environment variables synced",

View File

@ -157,6 +157,7 @@
"feedback": "反馈", "feedback": "反馈",
"waiting-mcp-server": "等待 MCP 服务器响应", "waiting-mcp-server": "等待 MCP 服务器响应",
"parallel-tool-calls": "允许模型在单轮回复中调用多个工具", "parallel-tool-calls": "允许模型在单轮回复中调用多个工具",
"parallel-test": "并行测试",
"proxy-server": "代理服务器", "proxy-server": "代理服务器",
"update-model-list": "更新模型列表", "update-model-list": "更新模型列表",
"preset-env-sync.success": "预设环境变量同步完成", "preset-env-sync.success": "预设环境变量同步完成",