support ai-mook

This commit is contained in:
锦恢 2025-06-30 20:25:16 +08:00
parent 1ce5b3a60a
commit 34a6001455
14 changed files with 192 additions and 37 deletions

View File

@ -58,7 +58,7 @@ export interface EnableToolItem {
} }
export interface ChatSetting { export interface ChatSetting {
modelIndex: number modelIndex?: number
systemPrompt: string systemPrompt: string
enableTools: EnableToolItem[] enableTools: EnableToolItem[]
temperature: number temperature: number

View File

@ -162,16 +162,15 @@ provide('tabStorage', tabStorage);
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
} }
.tools-dialog-container .el-switch__core { .el-switch__core {
border: 1px solid var(--main-color) !important; border: 1px solid var(--main-color) !important;
} }
.el-switch .el-switch__action {
.tools-dialog-container .el-switch .el-switch__action {
background-color: var(--main-color); background-color: var(--main-color);
} }
.tools-dialog-container .el-switch.is-checked .el-switch__action { .el-switch.is-checked .el-switch__action {
background-color: var(--sidebar); background-color: var(--sidebar);
} }

View File

@ -48,6 +48,7 @@ export class TaskLoop {
private bridge: MessageBridge; private bridge: MessageBridge;
private streamingContent: Ref<string>; private streamingContent: Ref<string>;
private streamingToolCalls: Ref<ToolCall[]>; private streamingToolCalls: Ref<ToolCall[]>;
private aborted = false;
private currentChatId = ''; private currentChatId = '';
private onError: (error: IErrorMssage) => void = (msg) => { }; private onError: (error: IErrorMssage) => void = (msg) => { };
@ -318,6 +319,7 @@ export class TaskLoop {
}); });
this.streamingContent.value = ''; this.streamingContent.value = '';
this.streamingToolCalls.value = []; this.streamingToolCalls.value = [];
this.aborted = true;
} }
/** /**
@ -545,6 +547,7 @@ export class TaskLoop {
maxEpochs = 50, maxEpochs = 50,
verbose = 0 verbose = 0
} = this.taskOptions || {}; } = this.taskOptions || {};
this.aborted = false;
for (let i = 0; i < maxEpochs; ++i) { for (let i = 0; i < maxEpochs; ++i) {
@ -570,6 +573,12 @@ export class TaskLoop {
// 发送请求 // 发送请求
const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter); const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter);
// 如果在调用过程中出发了 abort则直接中断
if (this.aborted) {
this.aborted = false;
break;
}
// 如果存在需要调度的工具 // 如果存在需要调度的工具
if (this.streamingToolCalls.value.length > 0) { if (this.streamingToolCalls.value.length > 0) {
@ -597,8 +606,19 @@ export class TaskLoop {
// ready to call tools // ready to call tools
toolCall = this.consumeToolCalls(toolCall); toolCall = this.consumeToolCalls(toolCall);
if (this.aborted) {
this.aborted = false;
break;
}
let toolCallResult = await handleToolCalls(toolCall); let toolCallResult = await handleToolCalls(toolCall);
if (this.aborted) {
this.aborted = false;
break;
}
// hook : finish call tools // hook : finish call tools
toolCallResult = this.consumeToolCalleds(toolCallResult); toolCallResult = this.consumeToolCalleds(toolCallResult);
@ -656,6 +676,11 @@ export class TaskLoop {
} }
} }
if (this.aborted) {
this.aborted = false;
break;
}
} else if (this.streamingContent.value) { } else if (this.streamingContent.value) {
tabStorage.messages.push({ tabStorage.messages.push({
role: 'assistant', role: 'assistant',

View File

@ -33,9 +33,36 @@
<el-button @click="resetForm"> <el-button @click="resetForm">
{{ t('reset') }} {{ t('reset') }}
</el-button> </el-button>
<el-button @click="generateMockData"> <el-button @click="generateMockData" :loading="mockLoading"
:disabled="loading || aiMockLoading || mockLoading">
{{ 'mook' }} {{ 'mook' }}
</el-button> </el-button>
<el-popover placement="top" width="350" trigger="click" v-model:visible="aiPromptVisible">
<template #reference>
<el-button :loading="aiMockLoading" :disabled="loading || aiMockLoading || mockLoading">
{{ 'ai-mook' }}
</el-button>
</template>
<div style="margin-bottom: 8px; font-weight: bold;">
{{ t('edit-ai-mook-prompt') }}
</div>
<el-input type="textarea" v-model="aiMookPrompt" :rows="2" style="margin-bottom: 8px;" />
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<el-switch
v-model="enableXmlWrapper"
style="margin-right: 8px;"
/>
<span style="opacity: 0.7;">XML</span>
</div>
<div style="text-align: right;">
<el-button size="small" @click="aiPromptVisible = false">{{ t('cancel') }}</el-button>
<el-button size="small" type="primary" :loading="aiMockLoading" @click="onAIMookConfirm">
{{ t('confirm') }}
</el-button>
</div>
</el-popover>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@ -44,7 +71,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue'; import { defineComponent, defineProps, watch, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import type { ToolStorage } from './tools'; import type { ToolStorage } from './tools';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp'; import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
@ -54,6 +81,10 @@ import { mcpClientAdapter } from '@/views/connect/core';
import { JSONSchemaFaker } from 'json-schema-faker'; import { JSONSchemaFaker } from 'json-schema-faker';
defineComponent({ name: 'tool-executor' }); defineComponent({ name: 'tool-executor' });
const mockLoading = ref(false);
const aiMockLoading = ref(false);
const aiPromptVisible = ref(false);
const enableXmlWrapper = ref(false);
const { t } = useI18n(); const { t } = useI18n();
@ -85,6 +116,7 @@ const currentTool = computed(() => {
} }
}); });
const aiMookPrompt = ref(`please call the tool ${currentTool.value?.name || ''} to make some test`);
const formRules = computed<FormRules>(() => { const formRules = computed<FormRules>(() => {
const rules: FormRules = {}; const rules: FormRules = {};
@ -132,27 +164,89 @@ const resetForm = () => {
formRef.value?.resetFields(); formRef.value?.resetFields();
}; };
import { TaskLoop } from '@/components/main-panel/chat/core/task-loop';
import type { ChatStorage } from '../chat/chat-box/chat';
const onAIMookConfirm = async () => {
aiPromptVisible.value = false;
await generateAIMockData(aiMookPrompt.value);
};
const generateAIMockData = async (prompt?: string) => {
if (!currentTool.value?.inputSchema) return;
aiMockLoading.value = true;
try {
const loop = new TaskLoop({ maxEpochs: 1 });
const usePrompt = prompt || `please call the tool ${currentTool.value.name} to make some test`;
const chatStorage = {
messages: [],
settings: {
temperature: 0.6,
systemPrompt: '',
enableTools: [{
name: currentTool.value.name,
description: currentTool.value.description,
inputSchema: currentTool.value.inputSchema,
enabled: true
}],
enableWebSearch: false,
contextLength: 5,
enableXmlWrapper: enableXmlWrapper.value,
parallelToolCalls: false
}
} as ChatStorage;
loop.setMaxEpochs(1);
let aiMockJson: any = undefined;
loop.registerOnToolCall(toolCall => {
if (toolCall.function?.name === currentTool.value?.name) {
try {
const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
aiMockJson = toolArgs;
} catch (e) {
ElMessage.error('AI 生成的 JSON 解析错误');
}
} else {
ElMessage.error('AI 调用了未知的工具');
}
loop.abort();
return toolCall;
});
loop.registerOnError(error => {
ElMessage.error(error + '');
});
await loop.start(chatStorage, usePrompt);
if (aiMockJson && typeof aiMockJson === 'object') {
Object.keys(aiMockJson).forEach(key => {
tabStorage.formData[key] = aiMockJson[key];
});
formRef.value?.clearValidate?.();
}
} finally {
aiMockLoading.value = false;
}
};
const generateMockData = async () => { const generateMockData = async () => {
if (!currentTool.value?.inputSchema) return; if (!currentTool.value?.inputSchema) return;
mockLoading.value = true;
// fakerjson-schema-faker try {
JSONSchemaFaker.option({ JSONSchemaFaker.option({
useDefaultValue: true, useDefaultValue: true,
alwaysFakeOptionals: true alwaysFakeOptionals: true
}); });
// mock
// TODO: as any ?
const mockData = await JSONSchemaFaker.resolve(currentTool.value.inputSchema as any) as any; const mockData = await JSONSchemaFaker.resolve(currentTool.value.inputSchema as any) as any;
// mock
Object.keys(mockData).forEach(key => { Object.keys(mockData).forEach(key => {
tabStorage.formData[key] = mockData[key]; tabStorage.formData[key] = mockData[key];
console.log(mockData[key]);
}); });
//
formRef.value?.clearValidate?.(); formRef.value?.clearValidate?.();
} finally {
mockLoading.value = false;
}
}; };
async function handleExecute() { async function handleExecute() {
@ -167,10 +261,15 @@ async function handleExecute() {
} }
} }
watch(currentTool, (tool) => {
aiMookPrompt.value = `please call the tool ${tool?.name || ''} to make some test`;
});
watch(() => tabStorage.currentToolName, () => { watch(() => tabStorage.currentToolName, () => {
initFormData(); initFormData();
resetForm(); resetForm();
}, { immediate: true }); }, { immediate: true });
</script> </script>
<style> <style>

View File

@ -20,8 +20,14 @@
<div v-else> <div v-else>
<!-- 展示原本的信息 --> <!-- 展示原本的信息 -->
<template v-if="!showRawJson"> <template v-if="!showRawJson && tabStorage.lastToolCallResponse">
{{tabStorage.lastToolCallResponse?.content.map(c => c.text).join('\n')}} <div
v-for="(c, idx) in tabStorage.lastToolCallResponse!.content"
:key="idx"
class="tool-call-block"
>
<pre class="tool-call-text">{{ c.text }}</pre>
</div>
</template> </template>
<!-- 展示 json --> <!-- 展示 json -->
@ -105,4 +111,21 @@ const showRawJson = ref(false);
padding: 5px 9px; padding: 5px 9px;
border-radius: .5em; border-radius: .5em;
} }
.tool-call-block {
margin-bottom: 12px;
padding: 10px 12px;
background: rgba(0,0,0,0.04);
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
}
.tool-call-text {
font-family: var(--code-font-family, monospace);
font-size: 15px;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
color: var(--el-text-color-primary, #222);
}
</style> </style>

View File

@ -183,5 +183,6 @@
"export": "تصدير", "export": "تصدير",
"export-filename": "اسم ملف التصدير", "export-filename": "اسم ملف التصدير",
"how-to-use": "كيفية الاستخدام؟", "how-to-use": "كيفية الاستخدام؟",
"is-required": "هو حقل مطلوب" "is-required": "هو حقل مطلوب",
"edit-ai-mook-prompt": "تحرير إشارات AI Mook"
} }

View File

@ -183,5 +183,6 @@
"export": "Exportieren", "export": "Exportieren",
"export-filename": "Exportdateiname", "export-filename": "Exportdateiname",
"how-to-use": "Wie benutzt man?", "how-to-use": "Wie benutzt man?",
"is-required": "ist ein Pflichtfeld" "is-required": "ist ein Pflichtfeld",
"edit-ai-mook-prompt": "AI Mook-Prompts bearbeiten"
} }

View File

@ -183,5 +183,6 @@
"export": "Export", "export": "Export",
"export-filename": "Export filename", "export-filename": "Export filename",
"how-to-use": "How to use?", "how-to-use": "How to use?",
"is-required": "is a required field" "is-required": "is a required field",
"edit-ai-mook-prompt": "Edit AI Mook prompts"
} }

View File

@ -183,5 +183,6 @@
"export": "Exporter", "export": "Exporter",
"export-filename": "Nom du fichier d'exportation", "export-filename": "Nom du fichier d'exportation",
"how-to-use": "Comment utiliser ?", "how-to-use": "Comment utiliser ?",
"is-required": "est un champ obligatoire" "is-required": "est un champ obligatoire",
"edit-ai-mook-prompt": "Modifier les invites AI Mook"
} }

View File

@ -183,5 +183,6 @@
"export": "エクスポート", "export": "エクスポート",
"export-filename": "エクスポートファイル名", "export-filename": "エクスポートファイル名",
"how-to-use": "使用方法", "how-to-use": "使用方法",
"is-required": "は必須フィールドです" "is-required": "は必須フィールドです",
"edit-ai-mook-prompt": "AI Mookプロンプトを編集"
} }

View File

@ -183,5 +183,6 @@
"export": "내보내기", "export": "내보내기",
"export-filename": "내보내기 파일 이름", "export-filename": "내보내기 파일 이름",
"how-to-use": "사용 방법?", "how-to-use": "사용 방법?",
"is-required": "는 필수 필드입니다" "is-required": "는 필수 필드입니다",
"edit-ai-mook-prompt": "AI Mook 프롬프트 편집"
} }

View File

@ -183,5 +183,6 @@
"export": "Экспорт", "export": "Экспорт",
"export-filename": "Имя файла экспорта", "export-filename": "Имя файла экспорта",
"how-to-use": "Как использовать?", "how-to-use": "Как использовать?",
"is-required": "является обязательным полем" "is-required": "является обязательным полем",
"edit-ai-mook-prompt": "Редактировать подсказки AI Mook"
} }

View File

@ -183,5 +183,6 @@
"export": "导出", "export": "导出",
"export-filename": "导出文件名", "export-filename": "导出文件名",
"how-to-use": "如何使用?", "how-to-use": "如何使用?",
"is-required": "是必填字段" "is-required": "是必填字段",
"edit-ai-mook-prompt": "编辑 AI Mook 提示词"
} }

View File

@ -183,5 +183,6 @@
"export": "導出", "export": "導出",
"export-filename": "導出文件名", "export-filename": "導出文件名",
"how-to-use": "如何使用?", "how-to-use": "如何使用?",
"is-required": "是必填欄位" "is-required": "是必填欄位",
"edit-ai-mook-prompt": "編輯AI Mook提示詞"
} }