对话中可以直接进行工具测试
This commit is contained in:
parent
493580ba3b
commit
fcf3b8cb9f
@ -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 () => {
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -45,7 +45,7 @@ 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,
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user