From 84e78560f48cb757b0ee8fad1c0549fddcdff88c Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 23 Jul 2025 17:27:39 +0800 Subject: [PATCH] refactor: streamline system prompt handling and introduce built-in tools (#7714) * refactor: streamline system prompt handling and introduce built-in tools - Removed the static SYSTEM_PROMPT_THRESHOLD from BaseApiClient and replaced it with a constant in constant.ts. - Updated API clients (AnthropicAPIClient, GeminiAPIClient, OpenAIApiClient, OpenAIResponseAPIClient) to simplify system prompt logic by eliminating unnecessary checks and using new utility functions for prompt building. - Introduced built-in tools functionality, including a new 'think' tool, to enhance the tool usage experience. - Refactored ApiService to integrate built-in tools and adjust system prompt modifications accordingly. - Added utility functions for managing built-in tools in mcp-tools.ts and created a new tools index for better organization. * refactor(tests): update prompt tests to use new buildSystemPromptWithTools function - Renamed the function used in prompt tests from buildSystemPrompt to buildSystemPromptWithTools to reflect recent changes in prompt handling. - Adjusted test cases to ensure compatibility with the updated function, maintaining the integrity of user prompt handling. * refactor(ApiService, mcp-tools, prompt): enhance tool usage and prompt handling - Updated ApiService to improve system prompt construction based on tool usage mode, ensuring clearer logic for tool integration. - Enhanced mcp-tools with a new response structure for the 'think' tool, allowing for better handling of tool responses. - Expanded prompt utility functions to include detailed instructions for using the 'think' tool, improving user guidance. - Refactored tests to validate new prompt building logic and tool integration, ensuring robust functionality across scenarios. * fix: enhance prompt * feat(McpToolChunkMiddleware): enhance tool call handling with built-in tool support - Added support for built-in tools in the parseAndCallTools function, allowing for conditional tool invocation based on tool type. - Implemented a check to return early if the tool call response is null, improving error handling. --- .../src/aiCore/clients/BaseApiClient.ts | 9 - .../clients/anthropic/AnthropicAPIClient.ts | 7 +- .../aiCore/clients/gemini/GeminiAPIClient.ts | 7 +- .../aiCore/clients/openai/OpenAIApiClient.ts | 5 - .../clients/openai/OpenAIResponseAPIClient.ts | 4 - .../middleware/core/McpToolChunkMiddleware.ts | 10 +- src/renderer/src/config/constant.ts | 1 + src/renderer/src/i18n/translate/el-gr.json | 2 +- src/renderer/src/i18n/translate/es-es.json | 2 +- src/renderer/src/i18n/translate/fr-fr.json | 2 +- src/renderer/src/i18n/translate/pt-pt.json | 2 +- src/renderer/src/services/ApiService.ts | 17 +- src/renderer/src/store/thunk/messageThunk.ts | 3 + src/renderer/src/tools/index.ts | 15 + src/renderer/src/tools/think.ts | 23 ++ src/renderer/src/types/index.ts | 1 + .../src/utils/__tests__/prompt.test.ts | 283 +++++++++++++++--- src/renderer/src/utils/mcp-tools.ts | 19 ++ src/renderer/src/utils/prompt.ts | 105 ++++++- src/renderer/traceWindow.html | 9 +- 20 files changed, 433 insertions(+), 93 deletions(-) create mode 100644 src/renderer/src/tools/index.ts create mode 100644 src/renderer/src/tools/think.ts diff --git a/src/renderer/src/aiCore/clients/BaseApiClient.ts b/src/renderer/src/aiCore/clients/BaseApiClient.ts index d4343da925..576381c875 100644 --- a/src/renderer/src/aiCore/clients/BaseApiClient.ts +++ b/src/renderer/src/aiCore/clients/BaseApiClient.ts @@ -62,12 +62,10 @@ export abstract class BaseApiClient< TSdkSpecificTool extends SdkTool = SdkTool > implements ApiClient { - private static readonly SYSTEM_PROMPT_THRESHOLD: number = 128 public provider: Provider protected host: string protected apiKey: string protected sdkInstance?: TSdkInstance - public useSystemPromptForTools: boolean = true constructor(provider: Provider) { this.provider = provider @@ -415,16 +413,9 @@ export abstract class BaseApiClient< return { tools } } - // If the number of tools exceeds the threshold, use the system prompt - if (mcpTools.length > BaseApiClient.SYSTEM_PROMPT_THRESHOLD) { - this.useSystemPromptForTools = true - return { tools } - } - // If the model supports function calling and tool usage is enabled if (isFunctionCallingModel(model) && enableToolUse) { tools = this.convertMcpToolsToSdkTools(mcpTools) - this.useSystemPromptForTools = false } return { tools } diff --git a/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts b/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts index 0e17d4d000..8d35ec3888 100644 --- a/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts +++ b/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts @@ -69,7 +69,6 @@ import { mcpToolsToAnthropicTools } from '@renderer/utils/mcp-tools' import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find' -import { buildSystemPrompt } from '@renderer/utils/prompt' import { BaseApiClient } from '../BaseApiClient' import { AnthropicStreamListener, RawStreamListener, RequestTransformer, ResponseChunkTransformer } from '../types' @@ -450,7 +449,7 @@ export class AnthropicAPIClient extends BaseApiClient< }> => { const { messages, mcpTools, maxTokens, streamOutput, enableWebSearch } = coreRequest // 1. 处理系统消息 - let systemPrompt = assistant.prompt + const systemPrompt = assistant.prompt // 2. 设置工具 const { tools } = this.setupToolsConfig({ @@ -459,10 +458,6 @@ export class AnthropicAPIClient extends BaseApiClient< enableToolUse: isEnabledToolUse(assistant) }) - if (this.useSystemPromptForTools) { - systemPrompt = await buildSystemPrompt(systemPrompt, mcpTools, assistant) - } - const systemMessage: TextBlockParam | undefined = systemPrompt ? { type: 'text', text: systemPrompt } : undefined diff --git a/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts b/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts index 9d8d32b08e..edc8a1190a 100644 --- a/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts +++ b/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts @@ -59,7 +59,6 @@ import { mcpToolsToGeminiTools } from '@renderer/utils/mcp-tools' import { findFileBlocks, findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' -import { buildSystemPrompt } from '@renderer/utils/prompt' import { defaultTimeout, MB } from '@shared/config/constant' import { BaseApiClient } from '../BaseApiClient' @@ -448,7 +447,7 @@ export class GeminiAPIClient extends BaseApiClient< }> => { const { messages, mcpTools, maxTokens, enableWebSearch, enableUrlContext, enableGenerateImage } = coreRequest // 1. 处理系统消息 - let systemInstruction = assistant.prompt + const systemInstruction = assistant.prompt // 2. 设置工具 const { tools } = this.setupToolsConfig({ @@ -457,10 +456,6 @@ export class GeminiAPIClient extends BaseApiClient< enableToolUse: isEnabledToolUse(assistant) }) - if (this.useSystemPromptForTools) { - systemInstruction = await buildSystemPrompt(assistant.prompt || '', mcpTools, assistant) - } - let messageContents: Content = { role: 'user', parts: [] } // Initialize messageContents const history: Content[] = [] // 3. 处理用户消息 diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts index 60d93024b5..22cd3f2f0b 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts @@ -52,7 +52,6 @@ import { openAIToolsToMcpTool } from '@renderer/utils/mcp-tools' import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find' -import { buildSystemPrompt } from '@renderer/utils/prompt' import OpenAI, { AzureOpenAI } from 'openai' import { ChatCompletionContentPart, ChatCompletionContentPartRefusal, ChatCompletionTool } from 'openai/resources' @@ -494,10 +493,6 @@ export class OpenAIAPIClient extends OpenAIBaseClient< enableToolUse: isEnabledToolUse(assistant) }) - if (this.useSystemPromptForTools) { - systemMessage.content = await buildSystemPrompt(systemMessage.content || '', mcpTools, assistant) - } - // 3. 处理用户消息 const userMessages: OpenAISdkMessageParam[] = [] if (typeof messages === 'string') { diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts index 450f6f4dfc..00e6444c06 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts @@ -36,7 +36,6 @@ import { openAIToolsToMcpTool } from '@renderer/utils/mcp-tools' import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find' -import { buildSystemPrompt } from '@renderer/utils/prompt' import { MB } from '@shared/config/constant' import { isEmpty } from 'lodash' import OpenAI, { AzureOpenAI } from 'openai' @@ -377,9 +376,6 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< enableToolUse: isEnabledToolUse(assistant) }) - if (this.useSystemPromptForTools) { - systemMessageInput.text = await buildSystemPrompt(systemMessageInput.text || '', mcpTools, assistant) - } systemMessageContent.push(systemMessageInput) systemMessage.content = systemMessageContent diff --git a/src/renderer/src/aiCore/middleware/core/McpToolChunkMiddleware.ts b/src/renderer/src/aiCore/middleware/core/McpToolChunkMiddleware.ts index 9e7c102bb6..c0d85b0fde 100644 --- a/src/renderer/src/aiCore/middleware/core/McpToolChunkMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/core/McpToolChunkMiddleware.ts @@ -3,6 +3,7 @@ import { MCPCallToolResponse, MCPTool, MCPToolResponse, Model, ToolCallResponse import { ChunkType, MCPToolCreatedChunk } from '@renderer/types/chunk' import { SdkMessageParam, SdkRawOutput, SdkToolCall } from '@renderer/types/sdk' import { + callBuiltInTool, callMCPTool, getMcpServerByTool, isToolAutoApproved, @@ -469,7 +470,10 @@ export async function parseAndCallTools( // 执行工具调用 try { const images: string[] = [] - const toolCallResponse = await callMCPTool(toolResponse, topicId, model.name) + // 根据工具类型选择不同的调用方式 + const toolCallResponse = toolResponse.tool.isBuiltIn + ? await callBuiltInTool(toolResponse) + : await callMCPTool(toolResponse, topicId, model.name) // 立即更新为done状态 upsertMCPToolResponse( @@ -482,6 +486,10 @@ export async function parseAndCallTools( onChunk! ) + if (!toolCallResponse) { + return + } + // 处理图片 for (const content of toolCallResponse.content) { if (content.type === 'image' && content.data) { diff --git a/src/renderer/src/config/constant.ts b/src/renderer/src/config/constant.ts index b40c35f51a..70dd66b4d8 100644 --- a/src/renderer/src/config/constant.ts +++ b/src/renderer/src/config/constant.ts @@ -4,6 +4,7 @@ export const DEFAULT_MAX_TOKENS = 4096 export const DEFAULT_KNOWLEDGE_DOCUMENT_COUNT = 6 export const DEFAULT_KNOWLEDGE_THRESHOLD = 0.0 export const DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT = 1 +export const SYSTEM_PROMPT_THRESHOLD = 128 export const platform = window.electron?.process?.platform export const isMac = platform === 'darwin' diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 38d62cd6d3..0bea2632c3 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -335,7 +335,6 @@ "provider": "Παρέχων", "reasoning_content": "Έχει σκεφτεί πολύ καλά", "regenerate": "Ξαναπαραγωγή", - "trace": "ίχνος", "rename": "Μετονομασία", "reset": "Επαναφορά", "save": "Αποθήκευση", @@ -347,6 +346,7 @@ "pinyin.desc": "Φθίνουσα ταξινόμηση κατά Πινγίν" }, "topics": "Θέματα", + "trace": "ίχνος", "warning": "Προσοχή", "you": "Εσείς" }, diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 1fd8428c78..a118386049 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -336,7 +336,6 @@ "provider": "Proveedor", "reasoning_content": "Pensamiento profundo", "regenerate": "Regenerar", - "trace": "Rastro", "rename": "Renombrar", "reset": "Restablecer", "save": "Guardar", @@ -348,6 +347,7 @@ "pinyin.desc": "Ordenar por pinyin descendente" }, "topics": "Temas", + "trace": "Rastro", "warning": "Advertencia", "you": "Usuario" }, diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 4e2c2a6095..27460ae457 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -335,7 +335,6 @@ "provider": "Fournisseur", "reasoning_content": "Réflexion approfondie", "regenerate": "Regénérer", - "trace": "Tracer", "rename": "Renommer", "reset": "Réinitialiser", "save": "Enregistrer", @@ -347,6 +346,7 @@ "pinyin.desc": "Сортировать по пиньинь в порядке убывания" }, "topics": "Sujets", + "trace": "Tracer", "warning": "Avertissement", "you": "Vous" }, diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 9a89762c2d..9856eb4965 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -337,7 +337,6 @@ "provider": "Fornecedor", "reasoning_content": "Pensamento profundo concluído", "regenerate": "Regenerar", - "trace": "Regenerar", "rename": "Renomear", "reset": "Redefinir", "save": "Salvar", @@ -349,6 +348,7 @@ "pinyin.desc": "Ordenar por Pinyin em ordem decrescente" }, "topics": "Tópicos", + "trace": "Regenerar", "warning": "Aviso", "you": "Você" }, diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index da9b54775d..c3f9e64a93 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { CompletionsParams } from '@renderer/aiCore/middleware/schemas' +import { SYSTEM_PROMPT_THRESHOLD } from '@renderer/config/constant' import { isEmbeddingModel, isGenerateImageModel, @@ -39,6 +40,7 @@ import { removeSpecialCharactersForTopicName } from '@renderer/utils' import { isAbortError } from '@renderer/utils/error' import { extractInfoFromXML, ExtractResults } from '@renderer/utils/extract' import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' +import { buildSystemPromptWithThinkTool, buildSystemPromptWithTools } from '@renderer/utils/prompt' import { findLast, isEmpty, takeRight } from 'lodash' import AiProvider from '../aiCore' @@ -345,7 +347,7 @@ async function fetchExternalTool( } // Get MCP tools (Fix duplicate declaration) - let mcpTools: MCPTool[] = [] // Initialize as empty array + let mcpTools: MCPTool[] = [] const allMcpServers = store.getState().mcp.servers || [] const activedMcpServers = allMcpServers.filter((s) => s.isActive) const assistantMcpServers = assistant.mcpServers || [] @@ -369,6 +371,19 @@ async function fetchExternalTool( .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value) .flat() + // 添加内置工具 + const { BUILT_IN_TOOLS } = await import('../tools') + mcpTools.push(...BUILT_IN_TOOLS) + + // 根据toolUseMode决定如何构建系统提示词 + const basePrompt = assistant.prompt + if (assistant.settings?.toolUseMode === 'prompt' || mcpTools.length > SYSTEM_PROMPT_THRESHOLD) { + // 提示词模式:需要完整的工具定义和思考指令 + assistant.prompt = buildSystemPromptWithTools(basePrompt, mcpTools) + } else { + // 原生函数调用模式:仅需要注入思考指令 + assistant.prompt = buildSystemPromptWithThinkTool(basePrompt) + } } catch (toolError) { logger.error('Error fetching MCP tools:', toolError as Error) } diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index 1a55e5f2c9..b40d3b555d 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -17,6 +17,7 @@ import { createTranslationBlock, resetAssistantMessage } from '@renderer/utils/messageUtils/create' +import { buildSystemPrompt } from '@renderer/utils/prompt' import { getTopicQueue } from '@renderer/utils/queue' import { waitForTopicQueue } from '@renderer/utils/queue' import { t } from 'i18next' @@ -877,6 +878,8 @@ const fetchAndProcessAssistantResponseImpl = async ( // } // } + assistant.prompt = await buildSystemPrompt(assistant.prompt || '', assistant) + callbacks = createCallbacks({ blockManager, dispatch, diff --git a/src/renderer/src/tools/index.ts b/src/renderer/src/tools/index.ts new file mode 100644 index 0000000000..69cddaae3e --- /dev/null +++ b/src/renderer/src/tools/index.ts @@ -0,0 +1,15 @@ +import { MCPTool } from '@renderer/types' + +import { thinkTool } from './think' + +export const BUILT_IN_TOOLS: MCPTool[] = [thinkTool] + +export function getBuiltInTool(name: string): MCPTool | undefined { + return BUILT_IN_TOOLS.find((tool) => tool.name === name || tool.id === name) +} + +export function isBuiltInTool(tool: MCPTool): boolean { + return tool.isBuiltIn === true +} + +export * from './think' diff --git a/src/renderer/src/tools/think.ts b/src/renderer/src/tools/think.ts new file mode 100644 index 0000000000..3da4c7f972 --- /dev/null +++ b/src/renderer/src/tools/think.ts @@ -0,0 +1,23 @@ +import { MCPTool } from '@renderer/types' + +export const thinkTool: MCPTool = { + id: 'dummy-server-think', + serverId: 'dummy-server', + serverName: 'Dummy Server', + name: 'think', + description: + 'Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed. For example, if you explore the repo and discover the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective. Alternatively, if you receive some test results, call this tool to brainstorm ways to fix the failing tests.', + isBuiltIn: true, + inputSchema: { + type: 'object', + title: 'Think Tool Input', + description: 'Input for the think tool', + required: ['thought'], + properties: { + thought: { + type: 'string', + description: 'Your thoughts.' + } + } + } +} diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 8b45d67f78..ad87aca488 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -672,6 +672,7 @@ export interface MCPTool { description?: string inputSchema: MCPToolInputSchema outputSchema?: z.infer + isBuiltIn?: boolean // 标识是否为内置工具,内置工具不需要通过MCP协议调用 } export interface MCPPromptArguments { diff --git a/src/renderer/src/utils/__tests__/prompt.test.ts b/src/renderer/src/utils/__tests__/prompt.test.ts index dd20ee5b0b..4e90685811 100644 --- a/src/renderer/src/utils/__tests__/prompt.test.ts +++ b/src/renderer/src/utils/__tests__/prompt.test.ts @@ -1,67 +1,260 @@ -import { type MCPTool } from '@renderer/types' -import { describe, expect, it } from 'vitest' +import { configureStore } from '@reduxjs/toolkit' +import { type Assistant, type MCPTool, type Model } from '@renderer/types' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { AvailableTools, buildSystemPrompt } from '../prompt' +import { + AvailableTools, + buildSystemPrompt, + buildSystemPromptWithThinkTool, + buildSystemPromptWithTools, + SYSTEM_PROMPT, + THINK_TOOL_PROMPT, + ToolUseExamples +} from '../prompt' + +// Mock window.api +const mockApi = { + system: { + getDeviceType: vi.fn() + }, + getAppInfo: vi.fn() +} + +vi.mock('@renderer/store', () => { + const mockStore = configureStore({ + reducer: { + settings: ( + state = { + language: 'zh-CN', + userName: 'MockUser' + } + ) => state + } + }) + return { + default: mockStore, + __esModule: true + } +}) + +// Helper to create a mock MCPTool +const createMockTool = (id: string, description: string, inputSchema: any = {}): MCPTool => ({ + id, + serverId: 'test-server', + serverName: 'Test Server', + name: id, + description, + inputSchema: { + type: 'object', + title: `${id}-schema`, + properties: {}, + ...inputSchema + } +}) + +// Helper to create a mock Assistant +const createMockAssistant = (name: string, modelName: string): Assistant => ({ + id: 'asst_mock_123', + name, + prompt: 'You are a helpful assistant.', + topics: [], + type: 'assistant', + model: { + id: modelName, + name: modelName, + provider: 'mock' + } as unknown as Model +}) + +// 设置全局 mocks +Object.defineProperty(window, 'api', { + value: mockApi, + writable: true +}) describe('prompt', () => { - describe('AvailableTools', () => { - it('should generate XML format for tools', () => { - const tools = [ - { id: 'test-tool', description: 'Test tool description', inputSchema: { type: 'object' } } as MCPTool - ] - const result = AvailableTools(tools) + const mockDate = new Date('2024-01-01T12:00:00Z') - expect(result).toContain('') - expect(result).toContain('') - expect(result).toContain('') - expect(result).toContain('test-tool') - expect(result).toContain('Test tool description') - expect(result).toContain('{"type":"object"}') + beforeEach(() => { + // 重置所有 mocks + vi.clearAllMocks() + vi.useFakeTimers() + vi.setSystemTime(mockDate) + + // 设置默认的 mock 返回值 + mockApi.system.getDeviceType.mockResolvedValue('macOS') + mockApi.getAppInfo.mockResolvedValue({ arch: 'darwin64' }) + }) + + afterEach(() => { + vi.useRealTimers() + vi.clearAllMocks() + }) + + describe('AvailableTools', () => { + it('should generate XML format for tools with strict equality', () => { + const tools = [createMockTool('test-tool', 'Test tool description')] + const result = AvailableTools(tools) + const expectedXml = ` + + + test-tool + Test tool description + + {"type":"object","title":"test-tool-schema","properties":{}} + + + +` + expect(result).toEqual(expectedXml) }) - it('should handle empty tools array', () => { + it('should handle empty tools array and return just the container tags', () => { const result = AvailableTools([]) + const expectedXml = ` - expect(result).toContain('') - expect(result).toContain('') - expect(result).not.toContain('') +` + expect(result).toEqual(expectedXml) }) }) describe('buildSystemPrompt', () => { - it('should build prompt with tools', async () => { - const userPrompt = 'Custom user system prompt' - const tools = [ - { id: 'test-tool', description: 'Test tool description', inputSchema: { type: 'object' } } as MCPTool - ] - const result = await buildSystemPrompt(userPrompt, tools) - - expect(result).toContain(userPrompt) - expect(result).toContain('test-tool') - expect(result).toContain('Test tool description') + it('should replace all variables correctly with strict equality', async () => { + const userPrompt = ` +以下是一些辅助信息: + - 日期和时间: {{datetime}}; + - 操作系统: {{system}}; + - 中央处理器架构: {{arch}}; + - 语言: {{language}}; + - 模型名称: {{model_name}}; + - 用户名称: {{username}}; +` + const assistant = createMockAssistant('MyAssistant', 'Super-Model-X') + const result = await buildSystemPrompt(userPrompt, assistant) + const expectedPrompt = ` +以下是一些辅助信息: + - 日期和时间: ${mockDate.toLocaleString()}; + - 操作系统: macOS; + - 中央处理器架构: darwin64; + - 语言: zh-CN; + - 模型名称: Super-Model-X; + - 用户名称: MockUser; +` + expect(result).toEqual(expectedPrompt) }) - it('should return user prompt without tools', async () => { - const userPrompt = 'Custom user system prompt' - const result = await buildSystemPrompt(userPrompt, []) + it('should handle API errors gracefully and use fallback values', async () => { + mockApi.system.getDeviceType.mockRejectedValue(new Error('API Error')) + mockApi.getAppInfo.mockRejectedValue(new Error('API Error')) - expect(result).toBe(userPrompt) + const userPrompt = 'System: {{system}}, Architecture: {{arch}}' + const result = await buildSystemPrompt(userPrompt) + const expectedPrompt = 'System: Unknown System, Architecture: Unknown Architecture' + expect(result).toEqual(expectedPrompt) }) - it('should handle null or undefined user prompt', async () => { - const tools = [ - { id: 'test-tool', description: 'Test tool description', inputSchema: { type: 'object' } } as MCPTool - ] + it('should handle non-string input gracefully', async () => { + const result = await buildSystemPrompt(null as any) + expect(result).toBe(null) + }) + }) - // 测试 userPrompt 为 null 的情况 - const resultNull = buildSystemPrompt(null as any, tools) - expect(resultNull).toBeDefined() - expect(resultNull).not.toContain('{{ USER_SYSTEM_PROMPT }}') + describe('Tool prompt composition', () => { + let basePrompt: string + let expectedBasePrompt: string + let tools: MCPTool[] - // 测试 userPrompt 为 undefined 的情况 - const resultUndefined = buildSystemPrompt(undefined as any, tools) - expect(resultUndefined).toBeDefined() - expect(resultUndefined).not.toContain('{{ USER_SYSTEM_PROMPT }}') + beforeEach(async () => { + const initialPrompt = ` + System Information: + - Date: {{date}} + - User: {{username}} + + Instructions: Be helpful. + ` + const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') + basePrompt = await buildSystemPrompt(initialPrompt, assistant) + expectedBasePrompt = ` + System Information: + - Date: ${mockDate.toLocaleDateString()} + - User: MockUser + + Instructions: Be helpful. + ` + tools = [createMockTool('web_search', 'Search the web')] + }) + + it('should build a full prompt for "prompt" toolUseMode', () => { + const finalPrompt = buildSystemPromptWithTools(basePrompt, tools) + const expectedFinalPrompt = SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', expectedBasePrompt) + .replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples) + .replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools)) + + expect(finalPrompt).toEqual(expectedFinalPrompt) + expect(finalPrompt).toContain('## Tool Use Formatting') + expect(finalPrompt).toContain('## Using the think tool') + }) + + it('should build a think-only prompt for native function calling mode', () => { + const finalPrompt = buildSystemPromptWithThinkTool(basePrompt) + const expectedFinalPrompt = THINK_TOOL_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', expectedBasePrompt) + + expect(finalPrompt).toEqual(expectedFinalPrompt) + expect(finalPrompt).not.toContain('## Tool Use Formatting') + expect(finalPrompt).toContain('## Using the think tool') + }) + + it('should return the original prompt if no tools are provided to buildSystemPromptWithTools', () => { + const result = buildSystemPromptWithTools(basePrompt, []) + expect(result).toBe(basePrompt) + }) + }) + + describe('buildSystemPromptWithTools', () => { + it('should build a full prompt for "prompt" toolUseMode', async () => { + const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') + const basePrompt = await buildSystemPrompt('Be helpful.', assistant) + const tools = [createMockTool('web_search', 'Search the web')] + + const finalPrompt = buildSystemPromptWithTools(basePrompt, tools) + const expectedFinalPrompt = SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', basePrompt) + .replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples) + .replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools)) + + expect(finalPrompt).toEqual(expectedFinalPrompt) + expect(finalPrompt).toContain('## Tool Use Formatting') + expect(finalPrompt).toContain('## Using the think tool') + }) + }) + + describe('buildSystemPromptWithThinkTool', () => { + it('should combine a template prompt with think tool instructions for native function calling', async () => { + // 1. 创建一个带变量的模板提示词,并处理它 + const initialPrompt = ` + System Information: + - Date: {{date}} + - User: {{username}} + + Instructions: Be helpful. + ` + const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') + const basePrompt = await buildSystemPrompt(initialPrompt, assistant) + const expectedBasePrompt = ` + System Information: + - Date: ${mockDate.toLocaleDateString()} + - User: MockUser + + Instructions: Be helpful. + ` + + // 2. 将处理过的提示词与思考工具结合 + const finalPrompt = buildSystemPromptWithThinkTool(basePrompt) + const expectedFinalPrompt = THINK_TOOL_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', expectedBasePrompt) + + // 3. 验证结果 + expect(finalPrompt).toEqual(expectedFinalPrompt) + expect(finalPrompt).not.toContain('## Tool Use Formatting') // 验证不包含工具定义 + expect(finalPrompt).toContain('## Using the think tool') // 验证包含思考指令 }) }) }) diff --git a/src/renderer/src/utils/mcp-tools.ts b/src/renderer/src/utils/mcp-tools.ts index ae081e3259..fa0a12c5f3 100644 --- a/src/renderer/src/utils/mcp-tools.ts +++ b/src/renderer/src/utils/mcp-tools.ts @@ -266,6 +266,25 @@ export function openAIToolsToMcpTool( return tool } +export async function callBuiltInTool(toolResponse: MCPToolResponse): Promise { + logger.info(`[BuiltIn] Calling Built-in Tool: ${toolResponse.tool.name}`, toolResponse.tool) + + if (toolResponse.tool.name === 'think') { + const thought = toolResponse.arguments?.thought + return { + isError: false, + content: [ + { + type: 'text', + text: (thought as string) || '' + } + ] + } + } + + return undefined +} + export async function callMCPTool( toolResponse: MCPToolResponse, topicId?: string, diff --git a/src/renderer/src/utils/prompt.ts b/src/renderer/src/utils/prompt.ts index a566f2a301..88ec126a1c 100644 --- a/src/renderer/src/utils/prompt.ts +++ b/src/renderer/src/utils/prompt.ts @@ -7,6 +7,47 @@ const logger = loggerService.withContext('Utils:Prompt') export const SYSTEM_PROMPT = `In this environment you have access to a set of tools you can use to answer the user's question. \ You can use one or more tools per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use. +## Using the think tool + +Before taking any action or responding to the user after receiving tool results, use the think tool as a scratchpad to: +- List the specific rules that apply to the current request +- Check if all required information is collected +- Verify that the planned action complies with all policies +- Iterate over tool results for correctness + +Here are some examples of what to iterate over inside the think tool: + +User wants to cancel flight ABC123 +- Need to verify: user ID, reservation ID, reason +- Check cancellation rules: + * Is it within 24h of booking? + * If not, check ticket class and insurance +- Verify no segments flown or are in the past +- Plan: collect missing info, verify rules, get confirmation + + + +User wants to book 3 tickets to NYC with 2 checked bags each +- Need user ID to check: + * Membership tier for baggage allowance + * Which payments methods exist in profile +- Baggage calculation: + * Economy class × 3 passengers + * If regular member: 1 free bag each → 3 extra bags = $150 + * If silver member: 2 free bags each → 0 extra bags = $0 + * If gold member: 3 free bags each → 0 extra bags = $0 +- Payment rules to verify: + * Max 1 travel certificate, 1 credit card, 3 gift cards + * All payment methods must be in profile + * Travel certificate remainder goes to waste +- Plan: +1. Get user ID +2. Verify membership level for bag fees +3. Check which payment methods in profile and if their combination is allowed +4. Calculate total: ticket price + any bag fees +5. Get explicit confirmation for booking + + ## Tool Use Formatting Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure: @@ -56,10 +97,55 @@ Here are the rules you should always follow to solve your task: # User Instructions {{ USER_SYSTEM_PROMPT }} - +Response in user query language. Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000. ` +export const THINK_TOOL_PROMPT = `{{ USER_SYSTEM_PROMPT }} + +## Using the think tool + +Before taking any action or responding to the user after receiving tool results, use the think tool as a scratchpad to: +- List the specific rules that apply to the current request +- Check if all required information is collected +- Verify that the planned action complies with all policies +- Iterate over tool results for correctness +- Response in user query language + +Here are some examples of what to iterate over inside the think tool: + +User wants to cancel flight ABC123 +- Need to verify: user ID, reservation ID, reason +- Check cancellation rules: + * Is it within 24h of booking? + * If not, check ticket class and insurance +- Verify no segments flown or are in the past +- Plan: collect missing info, verify rules, get confirmation + + + +User wants to book 3 tickets to NYC with 2 checked bags each +- Need user ID to check: + * Membership tier for baggage allowance + * Which payments methods exist in profile +- Baggage calculation: + * Economy class × 3 passengers + * If regular member: 1 free bag each → 3 extra bags = $150 + * If silver member: 2 free bags each → 0 extra bags = $0 + * If gold member: 3 free bags each → 0 extra bags = $0 +- Payment rules to verify: + * Max 1 travel certificate, 1 credit card, 3 gift cards + * All payment methods must be in profile + * Travel certificate remainder goes to waste +- Plan: +1. Get user ID +2. Verify membership level for bag fees +3. Check which payment methods in profile and if their combination is allowed +4. Calculate total: ticket price + any bag fees +5. Get explicit confirmation for booking + +` + export const ToolUseExamples = ` Here are a few examples using notional tools: --- @@ -151,11 +237,7 @@ ${availableTools} ` } -export const buildSystemPrompt = async ( - userSystemPrompt: string, - tools?: MCPTool[], - assistant?: Assistant -): Promise => { +export const buildSystemPrompt = async (userSystemPrompt: string, assistant?: Assistant): Promise => { if (typeof userSystemPrompt === 'string') { const now = new Date() if (userSystemPrompt.includes('{{date}}')) { @@ -223,11 +305,18 @@ export const buildSystemPrompt = async ( } } + return userSystemPrompt +} + +export const buildSystemPromptWithTools = (userSystemPrompt: string, tools?: MCPTool[]): string => { if (tools && tools.length > 0) { - return SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt) + return SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt || '') .replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples) .replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools)) } - return userSystemPrompt } + +export const buildSystemPromptWithThinkTool = (userSystemPrompt: string): string => { + return THINK_TOOL_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt || '') +} diff --git a/src/renderer/traceWindow.html b/src/renderer/traceWindow.html index 1813337471..2d3be8d607 100644 --- a/src/renderer/traceWindow.html +++ b/src/renderer/traceWindow.html @@ -1,9 +1,10 @@ - +