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

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() {
connectionArgs.commandString = 'uv run mcp run ../servers/bing-picture.py';
connectionArgs.commandString = 'uv run mcp run ../servers/main.py';
connectionMethods.current = 'STDIO';
setTimeout(async () => {

View File

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

View File

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

View File

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

View File

@ -73,7 +73,11 @@ onMounted(() => {
commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => {
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.lastToolCallResponse = undefined;
}

View File

@ -41,6 +41,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { tabs } from '../panel';
import { ToolStorage } from './tools';
import { markdownToHtml } from '../chat/markdown';
defineComponent({ name: 'tool-logger' });
const { t } = useI18n();
@ -64,6 +65,12 @@ const formattedJson = computed(() => {
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>
<style>

View File

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

View File

@ -45,8 +45,8 @@ export function loadPanels() {
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({
name: tab.name,
icon: tab.icon,

View File

@ -1,9 +1,30 @@
import { reactive } from 'vue';
import { pinkLog } from './util';
import { saveSetting } from '@/hook/setting';
import { markRaw, reactive } from 'vue';
import { createTab, debugModes, tabs } from '@/components/main-panel/panel';
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 llmManager = reactive({
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": [
{
"name": "交互测试",
@ -7,7 +7,82 @@
"type": "blank",
"componentIndex": 3,
"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": {
"modelIndex": 0,
"enableTools": [
@ -43,6 +118,27 @@
"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
}
}
}
]
}