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 {
modelIndex: number
modelIndex?: number
systemPrompt: string
enableTools: EnableToolItem[]
temperature: number

View File

@ -162,16 +162,15 @@ provide('tabStorage', tabStorage);
color: var(--el-text-color-secondary);
}
.tools-dialog-container .el-switch__core {
.el-switch__core {
border: 1px solid var(--main-color) !important;
}
.tools-dialog-container .el-switch .el-switch__action {
.el-switch .el-switch__action {
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);
}

View File

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

View File

@ -33,9 +33,36 @@
<el-button @click="resetForm">
{{ t('reset') }}
</el-button>
<el-button @click="generateMockData">
<el-button @click="generateMockData" :loading="mockLoading"
:disabled="loading || aiMockLoading || mockLoading">
{{ 'mook' }}
</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>
</div>
@ -44,7 +71,7 @@
<script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue';
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 type { ToolStorage } from './tools';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
@ -54,6 +81,10 @@ import { mcpClientAdapter } from '@/views/connect/core';
import { JSONSchemaFaker } from 'json-schema-faker';
defineComponent({ name: 'tool-executor' });
const mockLoading = ref(false);
const aiMockLoading = ref(false);
const aiPromptVisible = ref(false);
const enableXmlWrapper = ref(false);
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 rules: FormRules = {};
@ -132,27 +164,89 @@ const resetForm = () => {
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 () => {
if (!currentTool.value?.inputSchema) return;
// fakerjson-schema-faker
JSONSchemaFaker.option({
useDefaultValue: true,
alwaysFakeOptionals: true
});
// mock
// TODO: as any ?
const mockData = await JSONSchemaFaker.resolve(currentTool.value.inputSchema as any) as any;
// mock
Object.keys(mockData).forEach(key => {
tabStorage.formData[key] = mockData[key];
console.log(mockData[key]);
});
//
formRef.value?.clearValidate?.();
mockLoading.value = true;
try {
JSONSchemaFaker.option({
useDefaultValue: true,
alwaysFakeOptionals: true
});
const mockData = await JSONSchemaFaker.resolve(currentTool.value.inputSchema as any) as any;
Object.keys(mockData).forEach(key => {
tabStorage.formData[key] = mockData[key];
});
formRef.value?.clearValidate?.();
} finally {
mockLoading.value = false;
}
};
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, () => {
initFormData();
resetForm();
}, { immediate: true });
</script>
<style>

View File

@ -20,8 +20,14 @@
<div v-else>
<!-- 展示原本的信息 -->
<template v-if="!showRawJson">
{{tabStorage.lastToolCallResponse?.content.map(c => c.text).join('\n')}}
<template v-if="!showRawJson && tabStorage.lastToolCallResponse">
<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>
<!-- 展示 json -->
@ -105,4 +111,21 @@ const showRawJson = ref(false);
padding: 5px 9px;
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>

View File

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

View File

@ -183,5 +183,6 @@
"export": "Exportieren",
"export-filename": "Exportdateiname",
"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-filename": "Export filename",
"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-filename": "Nom du fichier d'exportation",
"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-filename": "エクスポートファイル名",
"how-to-use": "使用方法",
"is-required": "は必須フィールドです"
"is-required": "は必須フィールドです",
"edit-ai-mook-prompt": "AI Mookプロンプトを編集"
}

View File

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

View File

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

View File

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

View File

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