From d9131791620c58742093b497ad0ad59ee1a528b5 Mon Sep 17 00:00:00 2001 From: randy1568 Date: Wed, 26 Nov 2025 19:49:09 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(=E6=94=AF=E6=8C=81=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E7=9A=84Agent=E5=8F=AF=E8=A2=AB=E5=8A=A0=E8=BD=BD=E8=BF=90?= =?UTF-8?q?=E8=A1=8C):=20=E6=94=AF=E6=8C=81=E9=80=9A=E8=BF=87A2A=E9=9B=86?= =?UTF-8?q?=E6=88=90=E7=9A=84=E7=AC=AC=E4=B8=89=E6=96=B9Agent=E4=BB=A5?= =?UTF-8?q?=E9=80=9A=E7=94=A8Agent=E7=9A=84=E5=BD=A2=E5=BC=8F=E8=A2=AB?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presenter/builtInToolsPresenter/index.ts | 87 ++++++++-- .../builtInToolsPresenter/useA2AServerTool.ts | 148 ++++++++++++++++++ src/main/presenter/configPresenter/index.ts | 12 +- .../presenter/llmProviderPresenter/index.ts | 13 +- .../promptPresenter/sections/tool-use.ts | 8 +- src/main/presenter/promptPresenter/system.ts | 24 ++- src/main/presenter/threadPresenter/index.ts | 40 +++-- .../message/MessageBlockPermissionRequest.vue | 11 +- src/renderer/src/stores/chat.ts | 14 +- .../types/presenters/legacy.presenters.d.ts | 36 +++-- .../presenters/llmprovider.presenter.d.ts | 4 +- .../types/presenters/thread.presenter.d.ts | 7 +- 12 files changed, 337 insertions(+), 67 deletions(-) create mode 100644 src/main/presenter/builtInToolsPresenter/useA2AServerTool.ts diff --git a/src/main/presenter/builtInToolsPresenter/index.ts b/src/main/presenter/builtInToolsPresenter/index.ts index 0eb6466..298b35a 100644 --- a/src/main/presenter/builtInToolsPresenter/index.ts +++ b/src/main/presenter/builtInToolsPresenter/index.ts @@ -1,10 +1,18 @@ import { jsonrepair } from 'jsonrepair' -import { Tool, MCPToolDefinition, MCPToolCall, MCPToolResponse } from '@shared/presenter' +import { + Tool, + MCPToolDefinition, + MCPToolCall, + MCPToolResponse, + Agent, + IBuiltInToolsPresenter +} from '@shared/presenter' import { BuiltInToolDefinition, BuiltInToolResponse, validateToolArgs, buildRawData } from './base' import { readFileTool, executeReadFileTool } from './readFileTool' import { writeFileTool, executeWriteFileTool } from './writeFileTool' import { listFilesTool, executeListFilesTool } from './listFilesTool' import { executeCommandTool, executeCommandToolHandler } from './executeCommandTool' +import { useA2AServerTool, executeUseA2AServerToolHandler } from './useA2AServerTool' export const BUILT_IN_TOOL_SERVER_NAME = 'polymind-builtin' export const BUILT_IN_TOOL_SERVER_DESCRIPTION = 'PolyMind built-in tools' @@ -24,6 +32,14 @@ const builtInToolExecutors: Record = { [executeCommandTool.name]: executeCommandToolHandler } +const a2aBuiltInTools: Record = { + [useA2AServerTool.name]: useA2AServerTool +} + +const a2aBuiltInToolExecutors: Record = { + [useA2AServerTool.name]: executeUseA2AServerToolHandler +} + class BuiltInToolCallError extends Error { rawData: MCPToolResponse @@ -34,13 +50,36 @@ class BuiltInToolCallError extends Error { } } -export class BuiltInToolsPresenter { +export class BuiltInToolsPresenter implements IBuiltInToolsPresenter { + private getToolRegistry(currentAgent?: Agent): { + tools: Record + executors: Record + } { + const tools = { ...builtInTools } + const executors = { ...builtInToolExecutors } + if (currentAgent?.type === 'A2A') { + useA2AServerTool.description = currentAgent.description + if (currentAgent.skills.length > 0) { + useA2AServerTool.description += ` Proficient in the following skills: \n${currentAgent.skills + .map((skill) => { + return `${skill.name}=>${skill.description}` + }) + .join('\n')}.` + } + Object.assign(tools, a2aBuiltInTools) + Object.assign(executors, a2aBuiltInToolExecutors) + } + return { tools, executors } + } + async executeBuiltInTool( toolName: string, args: any, - toolCallId: string + toolCallId: string, + currentAgent?: Agent ): Promise { - const def = builtInTools[toolName] + const { tools, executors } = this.getToolRegistry(currentAgent) + const def = tools[toolName] let resolvedArgs = args if (def) { const check = validateToolArgs(def, args) @@ -58,8 +97,22 @@ export class BuiltInToolsPresenter { resolvedArgs = check.normalizedArgs } - const executor = builtInToolExecutors[toolName] + const executor = executors[toolName] if (executor) { + if (executor === executeUseA2AServerToolHandler) { + if (!currentAgent || !currentAgent.a2aURL) { + const failureMessage = 'use_a2a_server requires an A2A agent with a valid a2aURL' + const meta = { error: failureMessage, tool: toolName } + return { + toolCallId, + content: failureMessage, + success: false, + metadata: meta, + rawData: buildRawData(toolCallId, failureMessage, true, meta) + } + } + resolvedArgs = { ...resolvedArgs, currentAgent } + } return await executor(resolvedArgs, toolCallId) } const msg = `Unknown built-in tool: ${toolName}` @@ -73,8 +126,9 @@ export class BuiltInToolsPresenter { } } - async getBuiltInTools(): Promise { - const definitions = Object.values(builtInTools) + async getBuiltInTools(currentAgent?: Agent): Promise { + const { tools } = this.getToolRegistry(currentAgent) + const definitions = Object.values(tools) return definitions.map((def) => ({ name: def.name, description: def.description, @@ -98,7 +152,10 @@ export class BuiltInToolsPresenter { return toolName in builtInTools } - async callTool(toolCall: MCPToolCall): Promise<{ content: string; rawData: MCPToolResponse }> { + async callTool( + toolCall: MCPToolCall, + currentAgent?: Agent + ): Promise<{ content: string; rawData: MCPToolResponse }> { let parsedArguments: Record try { @@ -117,7 +174,8 @@ export class BuiltInToolsPresenter { const response = await this.executeBuiltInTool( toolCall.function.name, parsedArguments, - toolCall.id + toolCall.id, + currentAgent ) if (!response.success || response.rawData.isError) { throw new BuiltInToolCallError(response.content, response.rawData) @@ -134,13 +192,16 @@ export class BuiltInToolsPresenter { } } - async getBuiltInToolDefinitions(enabled: boolean = true): Promise { + async getBuiltInToolDefinitions( + enabled: boolean = true, + currentAgent?: Agent + ): Promise { if (!enabled) { return [] } try { - const tools = await this.getBuiltInTools() + const tools = await this.getBuiltInTools(currentAgent) return tools.map((tool) => this.mapToolToDefinition(tool)) } catch (error) { console.error('[BuiltInToolsPresenter] Failed to load built-in tools:', error) @@ -152,8 +213,8 @@ export class BuiltInToolsPresenter { * 将 MCPToolDefinition 转换为 XML 格式 * @returns XML 格式的工具定义字符串 */ - async convertToolsToXml(enabled: boolean = true): Promise { - const tools = await this.getBuiltInToolDefinitions(enabled) + async convertToolsToXml(enabled: boolean = true, currentAgent?: Agent): Promise { + const tools = await this.getBuiltInToolDefinitions(enabled, currentAgent) const xmlTools = tools .map((tool) => { const { name, description, parameters } = tool.function diff --git a/src/main/presenter/builtInToolsPresenter/useA2AServerTool.ts b/src/main/presenter/builtInToolsPresenter/useA2AServerTool.ts new file mode 100644 index 0000000..5891d33 --- /dev/null +++ b/src/main/presenter/builtInToolsPresenter/useA2AServerTool.ts @@ -0,0 +1,148 @@ +import { randomUUID } from 'crypto' +import { type A2AMessageSendParams, type A2APart, type A2AServerResponse } from '@shared/presenter' +import { BuiltInToolDefinition, BuiltInToolResponse, buildRawData } from './base' +import { presenter } from '..' + +export let useA2AServerTool: BuiltInToolDefinition = { + name: 'use_a2a_server', + description: '', // 动态传进来 + parameters: { + type: 'object', + properties: { + user_input_message: { + type: 'string', + description: "The complete input of the user's latest conversation message." + } + }, + required: ['user_input_message'] + } +} + +export async function executeUseA2AServerToolHandler( + args: any, + toolCallId: string +): Promise { + const { user_input_message, currentAgent } = args ?? {} + try { + if (!currentAgent || !currentAgent.a2aURL) { + throw new Error('Current agent with a valid a2aURL is required for use_a2a_server.') + } + if (typeof user_input_message !== 'string' || !user_input_message.trim()) { + throw new Error('The query argument cannot be empty.') + } + + const parts: A2APart[] = [{ type: 'text', text: user_input_message.trim() }] + + const params: A2AMessageSendParams = { + messageId: randomUUID(), + kind: 'message', + role: 'user', + parts + } + const result = await presenter.a2aPresenter.sendMessage(currentAgent.a2aURL, params) + const responses = await collectResponses(result) + const successContent = formatResponses(responses) + + const metadata = { + name: currentAgent.name, + description: currentAgent.description, + serverUrl: currentAgent.a2aURL, + query: user_input_message.trim(), + responses + } + + return { + toolCallId, + content: successContent, + success: true, + metadata, + rawData: buildRawData(toolCallId, successContent, false, metadata) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + const failureMessage = `Failed to execute use_a2a_server: ${message}` + const metadata = { error: message } + return { + toolCallId, + content: failureMessage, + success: false, + metadata, + rawData: buildRawData(toolCallId, failureMessage, true, metadata) + } + } +} + +async function collectResponses( + result: A2AServerResponse | AsyncGenerator +): Promise { + const responses: A2AServerResponse[] = [] + if (typeof result[Symbol.asyncIterator] === 'function') { + for await (const chunk of result as AsyncGenerator) { + if (chunk.kind === 'error') { + throw new Error( + `A2A server error ${chunk.error?.code ?? ''}: ${chunk.error?.message || 'Unknown error'}` + ) + } + responses.push(chunk) + } + } else { + result = result as A2AServerResponse + if (result.kind === 'error') { + throw new Error( + `A2A server error ${result.error?.code ?? ''}: ${result.error?.message || 'Unknown error'}` + ) + } + responses.push(result) + } + return responses +} + +function formatResponses(responses: A2AServerResponse[]): string { + if (responses.length === 0) { + throw new Error('no response chunk') + } + for (const response of [...responses].reverse()) { + switch (response.kind) { + case 'message': + return `${formatParts(response.message?.parts)}` + case 'task': { + const status = response.task?.status + const statusText = status ? `${formatParts(status.parts)}\n` : '' + const artifactsText = + response.task?.artifacts && response.task.artifacts.length > 0 + ? `${formatArtifacts(response.task.artifacts)}` + : '' + return `${statusText}${artifactsText}` + } + } + } + throw new Error('no response chunk') +} + +function formatParts(parts?: A2APart[] | null): string { + if (!parts || parts.length === 0) { + return '' + } + + const rendered = parts + .map((part) => { + if (part.text) { + return part.text + } + return '' + }) + .join('\n') + + return rendered || '' +} + +function formatArtifacts(artifacts?: { name?: string | null; parts: A2APart[] }[] | null): string { + if (!artifacts || artifacts.length === 0) { + return '' + } + return artifacts + .map((artifact) => { + return `${formatParts(artifact.parts)}` + }) + .join('\n') +} diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 6ab07ee..d6f84d2 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -1271,18 +1271,8 @@ export class ConfigPresenter implements IConfigPresenter { private async getBuildInSystemPrompt(agent?: Agent): Promise { // 获取内置的系统提示词 - let roleDefinition = '' - if (agent) { - roleDefinition += `Your name is ${agent.name},${agent.description}.` - if (agent.skills.length > 0) { - roleDefinition += `You have the following skills:\n` - for (const skill of agent.skills || []) { - roleDefinition += `- ${skill.name}=>${skill.description}\n` - } - } - } const useBuiltInToolsEnabled = this.getUseBuiltInToolsEnabled() - return await SYSTEM_PROMPT('', '', this.getLanguage(), '', useBuiltInToolsEnabled, roleDefinition) + return await SYSTEM_PROMPT('', '', this.getLanguage(), '', useBuiltInToolsEnabled, agent) } async getSystemPrompts(): Promise { diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index e1a2dc2..8851e47 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -12,7 +12,8 @@ import { ModelScopeMcpSyncOptions, ModelScopeMcpSyncResult, IConfigPresenter, - MCPToolDefinition + MCPToolDefinition, + Agent } from '@shared/presenter' import { ProviderChange, ProviderBatchUpdate } from '@shared/provider-operations' import { BaseLLMProvider } from './baseProvider' @@ -706,7 +707,8 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { verbosity?: 'low' | 'medium' | 'high', enableSearch?: boolean, forcedSearch?: boolean, - searchStrategy?: 'turbo' | 'max' + searchStrategy?: 'turbo' | 'max', + currentAgent?: Agent | null ): AsyncGenerator { console.log(`[Agent Loop] Starting agent loop for event: ${eventId} with model: ${modelId}`) if (!this.canStartNewStream()) { @@ -803,7 +805,10 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { const useBuiltInToolsEnabled = this.configPresenter.getUseBuiltInToolsEnabled() const [mcpTools, builtInTools] = await Promise.all([ presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools), - presenter.builtInToolsPresenter.getBuiltInToolDefinitions(useBuiltInToolsEnabled) + presenter.builtInToolsPresenter.getBuiltInToolDefinitions( + useBuiltInToolsEnabled, + currentAgent + ) ]) availableTools = [...mcpTools, ...builtInTools] @@ -1117,7 +1122,7 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { try { // Execute the tool via McpPresenter or builtInToolsPresenter const toolResponse = isBuiltInTools - ? await presenter.builtInToolsPresenter.callTool(mcpToolInput) + ? await presenter.builtInToolsPresenter.callTool(mcpToolInput, currentAgent) : await presenter.mcpPresenter.callTool(mcpToolInput) if (abortController.signal.aborted) break // Check after tool call returns diff --git a/src/main/presenter/promptPresenter/sections/tool-use.ts b/src/main/presenter/promptPresenter/sections/tool-use.ts index 43ecfbe..a16415d 100644 --- a/src/main/presenter/promptPresenter/sections/tool-use.ts +++ b/src/main/presenter/promptPresenter/sections/tool-use.ts @@ -1,8 +1,14 @@ +function getSystemPromptByA2ATool(toolsXML: string): string { + if (toolsXML.includes('use_a2a_server')) { + return "If the use_a2a_server tool exists in the list of available tools, prioritize using use_a2a_server to address the user's request based on the semantic meaning of the user's input." + } + return '' +} export function getSharedToolUseSection(toolsXML: string): string { return ` ==== # ToolUse -You have the ability to invoke external tools to assist in resolving user problems. +You have the ability to invoke external tools to assist in resolving user problems. ${getSystemPromptByA2ATool(toolsXML)} The list of available tools is defined in the tag: diff --git a/src/main/presenter/promptPresenter/system.ts b/src/main/presenter/promptPresenter/system.ts index 827951f..9822f34 100644 --- a/src/main/presenter/promptPresenter/system.ts +++ b/src/main/presenter/promptPresenter/system.ts @@ -8,6 +8,7 @@ import { addCustomInstructions, markdownFormattingSection } from './sections' +import { Agent } from '@shared/presenter' // Helper function to get prompt component, filtering out empty objects export function getPromptComponent( @@ -28,16 +29,29 @@ async function generatePrompt( language?: string, IgnoreInstructions?: string, useBuiltInToolsEnabled?: boolean, - roleDefinition?: string + agent?: Agent ): Promise { const promptSections: string[] = [] - if (roleDefinition) { + if (agent) { + let roleDefinition = '' + if (agent) { + roleDefinition += `Your name is ${agent.name},${agent.description}.` + if (agent.skills.length > 0) { + roleDefinition += `You have the following skills:\n` + for (const skill of agent.skills || []) { + roleDefinition += `- ${skill.name}=>${skill.description}\n` + } + } + } promptSections.push(roleDefinition) } promptSections.push(markdownFormattingSection()) if (useBuiltInToolsEnabled) { - const toolsXML = await presenter.builtInToolsPresenter.convertToolsToXml(useBuiltInToolsEnabled) + const toolsXML = await presenter.builtInToolsPresenter.convertToolsToXml( + useBuiltInToolsEnabled, + agent + ) promptSections.push(`${getSharedToolUseSection(toolsXML)}`) } @@ -72,7 +86,7 @@ export const SYSTEM_PROMPT = async ( language?: string, IgnoreInstructions?: string, useBuiltInToolsEnabled?: boolean, - roleDefinition?: string + agent?: Agent ): Promise => { return generatePrompt( cwd, @@ -80,6 +94,6 @@ export const SYSTEM_PROMPT = async ( language, IgnoreInstructions, useBuiltInToolsEnabled, - roleDefinition + agent ) } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index 056bf32..2d8746e 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -14,7 +14,8 @@ import { ChatMessage, ChatMessageContent, LLMAgentEventData, - AIScriptResult + AIScriptResult, + Agent } from '../../../shared/presenter' import { presenter } from '@/presenter' import { MessageManager } from './messageManager' @@ -1813,7 +1814,7 @@ export class ThreadPresenter implements IThreadPresenter { return results.map((result) => JSON.parse(result.content) as SearchResult) ?? [] } - async startStreamCompletion(conversationId: string, queryMsgId?: string) { + async startStreamCompletion(conversationId: string, currentAgent?: Agent, queryMsgId?: string) { const state = this.findGeneratingState(conversationId) if (!state) { console.warn('未找到状态,conversationId:', conversationId) @@ -1918,7 +1919,8 @@ export class ThreadPresenter implements IThreadPresenter { currentVerbosity, currentEnableSearch, currentForcedSearch, - currentSearchStrategy + currentSearchStrategy, + currentAgent ) for await (const event of stream) { const msg = event.data @@ -1942,7 +1944,7 @@ export class ThreadPresenter implements IThreadPresenter { throw error } } - async continueStreamCompletion(conversationId: string, queryMsgId: string) { + async continueStreamCompletion(conversationId: string, queryMsgId: string, currentAgent?: Agent) { const state = this.findGeneratingState(conversationId) if (!state) { console.warn('未找到状态,conversationId:', conversationId) @@ -2102,7 +2104,8 @@ export class ThreadPresenter implements IThreadPresenter { verbosity, enableSearch, forcedSearch, - searchStrategy + searchStrategy, + currentAgent ) for await (const event of stream) { const msg = event.data @@ -4090,7 +4093,8 @@ export class ThreadPresenter implements IThreadPresenter { toolCallId: string, granted: boolean, permissionType: 'read' | 'write' | 'all', - remember: boolean = true + remember: boolean = true, + currentAgent?: Agent ): Promise { console.log(`[ThreadPresenter] Handling permission response:`, { messageId, @@ -4186,7 +4190,7 @@ export class ThreadPresenter implements IThreadPresenter { } // 5. 现在重启agent loop - await this.restartAgentLoopAfterPermission(messageId) + await this.restartAgentLoopAfterPermission(messageId, currentAgent) } else { console.log( `[ThreadPresenter] Permission denied, ending generation for message: ${messageId}` @@ -4212,7 +4216,10 @@ export class ThreadPresenter implements IThreadPresenter { } // 重新启动agent loop (权限授予后) - private async restartAgentLoopAfterPermission(messageId: string): Promise { + private async restartAgentLoopAfterPermission( + messageId: string, + currentAgent?: Agent + ): Promise { console.log( `[ThreadPresenter] Restarting agent loop after permission for message: ${messageId}` ) @@ -4272,7 +4279,7 @@ export class ThreadPresenter implements IThreadPresenter { const state = this.generatingMessages.get(messageId) if (state) { console.log(`[ThreadPresenter] Message still in generating state, resuming from memory`) - await this.resumeStreamCompletion(conversationId, messageId) + await this.resumeStreamCompletion(conversationId, messageId, currentAgent) return } @@ -4296,7 +4303,7 @@ export class ThreadPresenter implements IThreadPresenter { console.log(`[ThreadPresenter] Created new generating state for message: ${messageId}`) // 启动新的流式完成 - await this.startStreamCompletion(conversationId, messageId) + await this.startStreamCompletion(conversationId, currentAgent, messageId) } catch (error) { console.error(`[ThreadPresenter] Failed to restart agent loop:`, error) @@ -4361,13 +4368,17 @@ export class ThreadPresenter implements IThreadPresenter { } // 恢复流式完成 (用于内存状态存在的情况) - private async resumeStreamCompletion(conversationId: string, messageId: string): Promise { + private async resumeStreamCompletion( + conversationId: string, + messageId: string, + currentAgent?: Agent + ): Promise { const state = this.generatingMessages.get(messageId) if (!state) { console.log( `[ThreadPresenter] No generating state found for ${messageId}, starting fresh agent loop` ) - await this.startStreamCompletion(conversationId) + await this.startStreamCompletion(conversationId, currentAgent) return } @@ -4411,7 +4422,7 @@ export class ThreadPresenter implements IThreadPresenter { `[ThreadPresenter] No pending tool call found after permission grant, using normal context` ) // 如果没有找到待执行的工具调用,使用正常流程 - await this.startStreamCompletion(conversationId, messageId) + await this.startStreamCompletion(conversationId, currentAgent, messageId) return } @@ -4454,7 +4465,8 @@ export class ThreadPresenter implements IThreadPresenter { verbosity, enableSearch, forcedSearch, - searchStrategy + searchStrategy, + currentAgent ) for await (const event of stream) { diff --git a/src/renderer/src/components/message/MessageBlockPermissionRequest.vue b/src/renderer/src/components/message/MessageBlockPermissionRequest.vue index daa6f8e..3a07d4d 100644 --- a/src/renderer/src/components/message/MessageBlockPermissionRequest.vue +++ b/src/renderer/src/components/message/MessageBlockPermissionRequest.vue @@ -81,15 +81,18 @@