对话中可以直接进行工具测试

This commit is contained in:
锦恢 2025-04-22 02:25:07 +08:00
parent 493580ba3b
commit fcf3b8cb9f
10 changed files with 181 additions and 31 deletions

View File

@ -29,7 +29,7 @@ bridge.addCommandListener('hello', data => {
function initDebug() { function initDebug() {
connectionArgs.commandString = 'uv run mcp run ../servers/bing-picture.py'; connectionArgs.commandString = 'uv run mcp run ../servers/main.py';
connectionMethods.current = 'STDIO'; connectionMethods.current = 'STDIO';
setTimeout(async () => { setTimeout(async () => {

View File

@ -42,7 +42,10 @@
<div v-for="(call, index) in message.tool_calls" :key="index" class="tool-call-item"> <div v-for="(call, index) in message.tool_calls" :key="index" class="tool-call-item">
<div class="tool-call-header"> <div class="tool-call-header">
<span class="tool-name">{{ call.function.name }}</span> <span class="tool-name">{{ call.function.name }}</span>
<span class="tool-type">{{ call.type }}</span> <span class="tool-type">{{ 'tool' }}</span>
<el-button @click="createTest(call)">
<span class="iconfont icon-send"></span>
</el-button>
</div> </div>
<div class="tool-arguments"> <div class="tool-arguments">
<div class="inner"> <div class="inner">
@ -154,7 +157,7 @@ import MessageMeta from './message-meta.vue';
// markdown.ts // markdown.ts
import { markdownToHtml, copyToClipboard } from './markdown'; import { markdownToHtml, copyToClipboard } from './markdown';
import { ChatCompletionChunk, TaskLoop } from './task-loop'; import { ChatCompletionChunk, TaskLoop } from './task-loop';
import { llmManager, llms } from '@/views/setting/llm'; import { createTest, llmManager, llms } from '@/views/setting/llm';
defineComponent({ name: 'chat' }); defineComponent({ name: 'chat' });
@ -390,14 +393,6 @@ const jsonResultToHtml = (jsonString: string) => {
return html; return html;
}; };
const formatToolArguments = (args: string) => {
try {
const parsed = JSON.parse(args);
return JSON.stringify(parsed, null, 2);
} catch {
return args;
}
};
</script> </script>

View File

@ -50,7 +50,7 @@ watch(
{ deep: true } { deep: true }
); );
function createTab(type: string, index: number): Tab { export function createTab(type: string, index: number): Tab {
let customName: string | null = null; let customName: string | null = null;
return { return {

View File

@ -3,7 +3,7 @@
<h3>{{ currentTool?.name }}</h3> <h3>{{ currentTool?.name }}</h3>
</div> </div>
<div class="tool-executor-container"> <div class="tool-executor-container">
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top"> <el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
<template v-if="currentTool?.inputSchema?.properties"> <template v-if="currentTool?.inputSchema?.properties">
<el-form-item <el-form-item
v-for="[name, property] in Object.entries(currentTool.inputSchema.properties)" v-for="[name, property] in Object.entries(currentTool.inputSchema.properties)"
@ -14,7 +14,7 @@
> >
<el-input <el-input
v-if="property.type === 'string'" v-if="property.type === 'string'"
v-model="formData[name]" v-model="tabStorage.formData[name]"
type="text" type="text"
:placeholder="t('enter') + ' ' + (property.title || name)" :placeholder="t('enter') + ' ' + (property.title || name)"
@keydown.enter.prevent="handleExecute" @keydown.enter.prevent="handleExecute"
@ -22,7 +22,7 @@
<el-input-number <el-input-number
v-else-if="property.type === 'number' || property.type === 'integer'" v-else-if="property.type === 'number' || property.type === 'integer'"
v-model="formData[name]" v-model="tabStorage.formData[name]"
controls-position="right" controls-position="right"
:placeholder="t('enter') + ' ' + (property.title || name)" :placeholder="t('enter') + ' ' + (property.title || name)"
@keydown.enter.prevent="handleExecute" @keydown.enter.prevent="handleExecute"
@ -30,7 +30,7 @@
<el-switch <el-switch
v-else-if="property.type === 'boolean'" v-else-if="property.type === 'boolean'"
v-model="formData[name]" v-model="tabStorage.formData[name]"
/> />
</el-form-item> </el-form-item>
</template> </template>
@ -53,8 +53,7 @@ import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { callTool, toolsManager, ToolStorage } from './tools'; import { callTool, toolsManager, ToolStorage } from './tools';
import { CasualRestAPI, ToolCallResponse } from '@/hook/type'; import { pinkLog } from '@/views/setting/util';
import { useMessageBridge } from '@/api/message-bridge';
defineComponent({ name: 'tool-executor' }); defineComponent({ name: 'tool-executor' });
@ -70,8 +69,11 @@ const props = defineProps({
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ToolStorage; const tabStorage = tab.storage as ToolStorage;
if (!tabStorage.formData) {
tabStorage.formData = {};
}
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const formData = ref<Record<string, any>>({});
const loading = ref(false); const loading = ref(false);
const currentTool = computed(() => { const currentTool = computed(() => {
@ -97,14 +99,38 @@ const formRules = computed<FormRules>(() => {
return rules; return rules;
}); });
const getDefaultValue = (property: any) => {
if (property.type === 'number' || property.type === 'integer') {
return 0;
} else if (property.type === 'boolean') {
return false;
} else {
return '';
}
};
const initFormData = () => { const initFormData = () => {
formData.value = {}; // inputSchema
// 1. key value schema
// 2. key value
if (!currentTool.value?.inputSchema?.properties) return; if (!currentTool.value?.inputSchema?.properties) return;
const newSchemaDataForm: Record<string, number | boolean | string> = {};
Object.entries(currentTool.value.inputSchema.properties).forEach(([name, property]) => { Object.entries(currentTool.value.inputSchema.properties).forEach(([name, property]) => {
formData.value[name] = (property.type === 'number' || property.type === 'integer') ? 0 : newSchemaDataForm[name] = getDefaultValue(property);
property.type === 'boolean' ? false : ''; let originType: string = typeof tabStorage.formData[name];
if (originType === 'number') {
originType = 'integer';
}
if (tabStorage.formData[name] !== undefined && originType === property.type) {
newSchemaDataForm[name] = tabStorage.formData[name];
}
}); });
tabStorage.formData = newSchemaDataForm;
}; };
const resetForm = () => { const resetForm = () => {
@ -114,7 +140,7 @@ const resetForm = () => {
async function handleExecute() { async function handleExecute() {
if (!currentTool.value) return; if (!currentTool.value) return;
const toolResponse = await callTool(tabStorage.currentToolName, formData.value); const toolResponse = await callTool(tabStorage.currentToolName, tabStorage.formData);
tabStorage.lastToolCallResponse = toolResponse; tabStorage.lastToolCallResponse = toolResponse;
} }

View File

@ -73,7 +73,11 @@ onMounted(() => {
commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => { commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => {
toolsManager.tools = data.msg.tools || []; toolsManager.tools = data.msg.tools || [];
if (toolsManager.tools.length > 0) { const targetTool = toolsManager.tools.find((tool) => {
return tool.name === tabStorage.currentToolName;
});
if (targetTool === undefined) {
tabStorage.currentToolName = toolsManager.tools[0].name; tabStorage.currentToolName = toolsManager.tools[0].name;
tabStorage.lastToolCallResponse = undefined; tabStorage.lastToolCallResponse = undefined;
} }

View File

@ -41,6 +41,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ToolStorage } from './tools'; import { ToolStorage } from './tools';
import { markdownToHtml } from '../chat/markdown';
defineComponent({ name: 'tool-logger' }); defineComponent({ name: 'tool-logger' });
const { t } = useI18n(); const { t } = useI18n();
@ -64,6 +65,12 @@ const formattedJson = computed(() => {
return 'Invalid JSON'; return 'Invalid JSON';
} }
}); });
const jsonResultToHtml = (jsonString: string) => {
const formattedJson = JSON.stringify(JSON.parse(jsonString), null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```');
return html;
};
</script> </script>
<style> <style>

View File

@ -12,6 +12,7 @@ export const toolsManager = reactive<{
export interface ToolStorage { export interface ToolStorage {
currentToolName: string; currentToolName: string;
lastToolCallResponse?: ToolCallResponse; lastToolCallResponse?: ToolCallResponse;
formData: Record<string, number | string | boolean>;
} }
const bridge = useMessageBridge(); const bridge = useMessageBridge();

View File

@ -45,8 +45,8 @@ export function loadPanels() {
for (const tab of persistTab.tabs || []) { for (const tab of persistTab.tabs || []) {
const component = tab.componentIndex >= 0? debugModes[tab.componentIndex] : undefined; const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined;
tabs.content.push({ tabs.content.push({
name: tab.name, name: tab.name,
icon: tab.icon, icon: tab.icon,

View File

@ -1,9 +1,30 @@
import { reactive } from 'vue'; import { markRaw, reactive } from 'vue';
import { pinkLog } from './util'; import { createTab, debugModes, tabs } from '@/components/main-panel/panel';
import { saveSetting } from '@/hook/setting'; import { ToolStorage } from '@/components/main-panel/tool/tools';
import { ToolCall } from '@/components/main-panel/chat/chat';
import I18n from '@/i18n';
const { t } = I18n.global;
export const llms = reactive<any[]>([]); export const llms = reactive<any[]>([]);
export const llmManager = reactive({ export const llmManager = reactive({
currentModelIndex: 0, currentModelIndex: 0,
}); });
export function createTest(call: ToolCall) {
const tab = createTab('tool', 0);
tab.componentIndex = 2;
tab.component = markRaw(debugModes[2]);
tab.icon = 'icon-tool';
tab.name = t("tools");
1
const storage: ToolStorage = {
currentToolName: call.function.name,
formData: JSON.parse(call.function.arguments)
};
tab.storage = storage;
tabs.content.push(tab);
tabs.activeIndex = tabs.content.length - 1;
}

View File

@ -1,5 +1,5 @@
{ {
"currentIndex": 0, "currentIndex": 1,
"tabs": [ "tabs": [
{ {
"name": "交互测试", "name": "交互测试",
@ -7,7 +7,82 @@
"type": "blank", "type": "blank",
"componentIndex": 3, "componentIndex": 3,
"storage": { "storage": {
"messages": [], "messages": [
{
"role": "user",
"content": "请问杭州的天气",
"extraInfo": {
"created": 1745258229256,
"serverName": "deepseek"
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_0_57ad3eab-cd9d-4403-bdf0-31d00b83b98c",
"index": 0,
"type": "function",
"function": {
"name": "get_weather_by_city_code",
"arguments": "{\"city_code\":101210101}"
}
}
],
"extraInfo": {
"created": 1745258234470,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 570,
"completion_tokens": 26,
"total_tokens": 596,
"prompt_tokens_details": {
"cached_tokens": 512
},
"prompt_cache_hit_tokens": 512,
"prompt_cache_miss_tokens": 58
}
}
},
{
"role": "tool",
"tool_call_id": "call_0_57ad3eab-cd9d-4403-bdf0-31d00b83b98c",
"content": "[{\"type\":\"text\",\"text\":\"CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='21', wd='', ws='', sd='92%', aqi='13', weather='阴')\"}]",
"extraInfo": {
"created": 1745258234522,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 570,
"completion_tokens": 26,
"total_tokens": 596,
"prompt_tokens_details": {
"cached_tokens": 512
},
"prompt_cache_hit_tokens": 512,
"prompt_cache_miss_tokens": 58
}
}
},
{
"role": "assistant",
"content": "杭州的天气信息如下:\n\n- 城市:杭州\n- 温度21°C\n- 天气状况:阴\n- 湿度92%\n- 空气质量指数 (AQI)13优秀\n\n天气较为舒适适合出行",
"extraInfo": {
"created": 1745258240571,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 660,
"completion_tokens": 52,
"total_tokens": 712,
"prompt_tokens_details": {
"cached_tokens": 576
},
"prompt_cache_hit_tokens": 576,
"prompt_cache_miss_tokens": 84
}
}
}
],
"settings": { "settings": {
"modelIndex": 0, "modelIndex": 0,
"enableTools": [ "enableTools": [
@ -43,6 +118,27 @@
"systemPrompt": "" "systemPrompt": ""
} }
} }
},
{
"name": "工具",
"icon": "icon-tool",
"type": "tool",
"componentIndex": 2,
"storage": {
"currentToolName": "get_weather_by_city_code",
"formData": {
"city_code": 101210101
},
"lastToolCallResponse": {
"content": [
{
"type": "text",
"text": "CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='20.7', wd='', ws='', sd='93%', aqi='13', weather='阴')"
}
],
"isError": false
}
}
} }
] ]
} }