增加任务循环
This commit is contained in:
parent
e095af2d8c
commit
0670cf8318
@ -29,6 +29,7 @@
|
||||
- [ ] 支持通过大模型进行在线验证
|
||||
- [ ] 支持 completion/complete 协议字段
|
||||
- [ ] 支持 对用户对应服务器的调试工作内容进行保存
|
||||
- [ ] 高危操作权限确认
|
||||
|
||||
|
||||
## Dev
|
||||
|
34
renderer/package-lock.json
generated
34
renderer/package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"vue-router": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
@ -31,7 +32,7 @@
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"prettier": "^2.4.1",
|
||||
"typescript": "^5.9.0-dev.20250407",
|
||||
"typescript": "^4.4.3",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0"
|
||||
}
|
||||
@ -2357,6 +2358,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
|
||||
@ -2372,6 +2379,22 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz",
|
||||
@ -12341,17 +12364,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.0-dev.20250407",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250407.tgz",
|
||||
"integrity": "sha512-JW8/Our6MR+QYS3M134UaLWtEYdVXWzwlbg6rj3fmF9TppADEdaSNiJK90M2wmfSuu5j8Nefk93oSrZF03JkGw==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
|
@ -18,6 +18,7 @@
|
||||
"vue-router": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
@ -31,7 +32,7 @@
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"prettier": "^2.4.1",
|
||||
"typescript": "^5.9.0-dev.20250407",
|
||||
"typescript": "^4.4.3",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0"
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ import { ToolItem } from "@/hook/type";
|
||||
import { ref } from "vue";
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
role: 'user' | 'assistant' | 'system' | 'tool';
|
||||
content: string;
|
||||
tool_call_id?: string
|
||||
name?: string // 工具名称,当 role 为 tool
|
||||
}
|
||||
|
||||
// 新增状态和工具数据
|
||||
@ -27,6 +29,12 @@ export interface ChatStorage {
|
||||
settings: ChatSetting
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id?: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
}
|
||||
|
||||
export const allTools = ref<ToolItem[]>([]);
|
||||
|
||||
export function getToolSchema(enableTools: EnableToolItem[]) {
|
||||
@ -36,12 +44,11 @@ export function getToolSchema(enableTools: EnableToolItem[]) {
|
||||
const tool = allTools.value[i];
|
||||
|
||||
toolsSchema.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: "function",
|
||||
properties: tool.inputSchema.properties,
|
||||
required: tool.inputSchema.required
|
||||
parameters: tool.inputSchema
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -29,7 +29,10 @@
|
||||
<div class="message-content" v-else>
|
||||
<div class="message-role">Tool</div>
|
||||
<div class="message-text">
|
||||
<span>Tool</span>
|
||||
<div v-if="streamingToolCalls.length > 0">
|
||||
<span>正在调用工具: {{ streamingToolCalls[0].name }}</span>
|
||||
</div>
|
||||
<span v-else>{{ message.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -74,7 +77,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useMessageBridge } from "@/api/message-bridge";
|
||||
import { ElMessage, ScrollbarInstance } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { ChatMessage, ChatStorage, getToolSchema } from './chat';
|
||||
import { ChatMessage, ChatStorage, getToolSchema, ToolCall } from './chat';
|
||||
|
||||
import Setting from './setting.vue';
|
||||
import { llmManager, llms } from '@/views/setting/llm';
|
||||
@ -115,7 +118,10 @@ if (!tabStorage.messages) {
|
||||
}
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
||||
const streamingContent = ref('');
|
||||
const streamingToolCalls = ref<ToolCall[]>([]);
|
||||
|
||||
const chatContainerRef = ref<any>(null);
|
||||
const messageListRef = ref<any>(null);
|
||||
|
||||
@ -227,11 +233,45 @@ const handleSend = () => {
|
||||
return;
|
||||
}
|
||||
const { chunk } = data.msg;
|
||||
|
||||
const content = chunk.choices[0]?.delta?.content || '';
|
||||
const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
|
||||
|
||||
if (content) {
|
||||
streamingContent.value += content;
|
||||
scrollToBottom(); // 内容更新时滚动到底部
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
if (toolCall) {
|
||||
if (toolCall.index === 0) {
|
||||
// 新的工具调用开始
|
||||
streamingToolCalls.value = [{
|
||||
id: toolCall.id,
|
||||
name: toolCall.function?.name || '',
|
||||
arguments: toolCall.function?.arguments || ''
|
||||
}];
|
||||
} else {
|
||||
// 累积现有工具调用的信息
|
||||
const currentCall = streamingToolCalls.value[toolCall.index];
|
||||
if (currentCall) {
|
||||
if (toolCall.id) {
|
||||
currentCall.id = toolCall.id;
|
||||
}
|
||||
if (toolCall.function?.name) {
|
||||
currentCall.name = toolCall.function.name;
|
||||
}
|
||||
if (toolCall.function?.arguments) {
|
||||
currentCall.arguments += toolCall.function.arguments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finishReason = chunk.choices[0]?.finish_reason;
|
||||
if (finishReason === 'tool_calls') {
|
||||
// 工具调用完成,这里可以处理工具调用
|
||||
console.log('Tool calls completed:', streamingToolCalls.value);
|
||||
streamingToolCalls.value = [];
|
||||
}
|
||||
}, { once: false });
|
||||
|
||||
@ -249,6 +289,19 @@ const handleSend = () => {
|
||||
});
|
||||
streamingContent.value = '';
|
||||
}
|
||||
// 如果有工具调用结果,也加入消息列表
|
||||
if (streamingToolCalls.value.length > 0) {
|
||||
streamingToolCalls.value.forEach(tool => {
|
||||
if (tool.id) {
|
||||
tabStorage.messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: tool.id,
|
||||
content: tool.arguments
|
||||
});
|
||||
}
|
||||
});
|
||||
streamingToolCalls.value = [];
|
||||
}
|
||||
isLoading.value = false;
|
||||
chunkHandler();
|
||||
}, { once: true });
|
||||
|
116
renderer/src/components/main-panel/chat/task-loop.ts
Normal file
116
renderer/src/components/main-panel/chat/task-loop.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { Ref } from "vue";
|
||||
import { ToolCall, ChatMessage } from "./chat";
|
||||
import { useMessageBridge } from "@/api/message-bridge";
|
||||
|
||||
export class TaskLoop {
|
||||
private bridge = useMessageBridge();
|
||||
|
||||
constructor(
|
||||
private readonly streamingContent: Ref<string>,
|
||||
private readonly streamingToolCalls: Ref<ToolCall[]>,
|
||||
private readonly messages: ChatMessage[],
|
||||
private readonly onError: (msg: string) => void
|
||||
) {}
|
||||
|
||||
private handleToolCalls(toolCalls: ToolCall[]) {
|
||||
// 这里预留给调用方实现工具执行逻辑
|
||||
return Promise.resolve("工具执行结果");
|
||||
}
|
||||
|
||||
private continueConversation(chatData: any) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => {
|
||||
if (data.code !== 200) {
|
||||
this.onError(data.msg || '请求模型服务时发生错误');
|
||||
reject(new Error(data.msg));
|
||||
return;
|
||||
}
|
||||
const { chunk } = data.msg;
|
||||
|
||||
const content = chunk.choices[0]?.delta?.content || '';
|
||||
const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
|
||||
|
||||
if (content) {
|
||||
this.streamingContent.value += content;
|
||||
}
|
||||
|
||||
if (toolCall) {
|
||||
if (toolCall.index === 0) {
|
||||
this.streamingToolCalls.value = [{
|
||||
id: toolCall.id,
|
||||
name: toolCall.function?.name || '',
|
||||
arguments: toolCall.function?.arguments || ''
|
||||
}];
|
||||
} else {
|
||||
const currentCall = this.streamingToolCalls.value[toolCall.index];
|
||||
if (currentCall) {
|
||||
if (toolCall.id) currentCall.id = toolCall.id;
|
||||
if (toolCall.function?.name) currentCall.name = toolCall.function.name;
|
||||
if (toolCall.function?.arguments) currentCall.arguments += toolCall.function.arguments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { once: false });
|
||||
|
||||
this.bridge.addCommandListener('llm/chat/completions/done', async data => {
|
||||
if (data.code !== 200) {
|
||||
this.onError(data.msg || '模型服务处理完成但返回错误');
|
||||
reject(new Error(data.msg));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.streamingContent.value) {
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: this.streamingContent.value
|
||||
});
|
||||
this.streamingContent.value = '';
|
||||
}
|
||||
|
||||
if (this.streamingToolCalls.value.length > 0) {
|
||||
try {
|
||||
const toolResult = await this.handleToolCalls(this.streamingToolCalls.value);
|
||||
|
||||
// 将工具执行结果添加到消息列表
|
||||
this.streamingToolCalls.value.forEach(tool => {
|
||||
if (tool.id) {
|
||||
this.messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: tool.id,
|
||||
content: toolResult
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 继续与模型对话
|
||||
await this.continueConversation(chatData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
} finally {
|
||||
this.streamingToolCalls.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
chunkHandler();
|
||||
resolve();
|
||||
}, { once: true });
|
||||
|
||||
this.bridge.postMessage({
|
||||
command: 'llm/chat/completions',
|
||||
data: chatData
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async start(chatData: any) {
|
||||
this.streamingContent.value = '';
|
||||
this.streamingToolCalls.value = [];
|
||||
|
||||
try {
|
||||
await this.continueConversation(chatData);
|
||||
} catch (error) {
|
||||
this.onError(error instanceof Error ? error.message : '未知错误');
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,9 @@ export async function chatCompletionHandler(client: MCPClient | undefined, data:
|
||||
apiKey
|
||||
});
|
||||
|
||||
console.log(tools);
|
||||
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model,
|
||||
messages,
|
||||
|
@ -10,11 +10,7 @@
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "test"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "错误: OpenAI API error: 422 Failed to deserialize the JSON body into the target type: tools[0]: missing field `type` at line 31 column 5"
|
||||
"content": "test add"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user