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.
This commit is contained in:
SuYao 2025-07-23 17:27:39 +08:00 committed by GitHub
parent 82bdbaa0f4
commit 84e78560f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 433 additions and 93 deletions

View File

@ -62,12 +62,10 @@ export abstract class BaseApiClient<
TSdkSpecificTool extends SdkTool = SdkTool TSdkSpecificTool extends SdkTool = SdkTool
> implements ApiClient<TSdkInstance, TSdkParams, TRawOutput, TRawChunk, TMessageParam, TToolCall, TSdkSpecificTool> > implements ApiClient<TSdkInstance, TSdkParams, TRawOutput, TRawChunk, TMessageParam, TToolCall, TSdkSpecificTool>
{ {
private static readonly SYSTEM_PROMPT_THRESHOLD: number = 128
public provider: Provider public provider: Provider
protected host: string protected host: string
protected apiKey: string protected apiKey: string
protected sdkInstance?: TSdkInstance protected sdkInstance?: TSdkInstance
public useSystemPromptForTools: boolean = true
constructor(provider: Provider) { constructor(provider: Provider) {
this.provider = provider this.provider = provider
@ -415,16 +413,9 @@ export abstract class BaseApiClient<
return { tools } 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 the model supports function calling and tool usage is enabled
if (isFunctionCallingModel(model) && enableToolUse) { if (isFunctionCallingModel(model) && enableToolUse) {
tools = this.convertMcpToolsToSdkTools(mcpTools) tools = this.convertMcpToolsToSdkTools(mcpTools)
this.useSystemPromptForTools = false
} }
return { tools } return { tools }

View File

@ -69,7 +69,6 @@ import {
mcpToolsToAnthropicTools mcpToolsToAnthropicTools
} from '@renderer/utils/mcp-tools' } from '@renderer/utils/mcp-tools'
import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find' import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find'
import { buildSystemPrompt } from '@renderer/utils/prompt'
import { BaseApiClient } from '../BaseApiClient' import { BaseApiClient } from '../BaseApiClient'
import { AnthropicStreamListener, RawStreamListener, RequestTransformer, ResponseChunkTransformer } from '../types' import { AnthropicStreamListener, RawStreamListener, RequestTransformer, ResponseChunkTransformer } from '../types'
@ -450,7 +449,7 @@ export class AnthropicAPIClient extends BaseApiClient<
}> => { }> => {
const { messages, mcpTools, maxTokens, streamOutput, enableWebSearch } = coreRequest const { messages, mcpTools, maxTokens, streamOutput, enableWebSearch } = coreRequest
// 1. 处理系统消息 // 1. 处理系统消息
let systemPrompt = assistant.prompt const systemPrompt = assistant.prompt
// 2. 设置工具 // 2. 设置工具
const { tools } = this.setupToolsConfig({ const { tools } = this.setupToolsConfig({
@ -459,10 +458,6 @@ export class AnthropicAPIClient extends BaseApiClient<
enableToolUse: isEnabledToolUse(assistant) enableToolUse: isEnabledToolUse(assistant)
}) })
if (this.useSystemPromptForTools) {
systemPrompt = await buildSystemPrompt(systemPrompt, mcpTools, assistant)
}
const systemMessage: TextBlockParam | undefined = systemPrompt const systemMessage: TextBlockParam | undefined = systemPrompt
? { type: 'text', text: systemPrompt } ? { type: 'text', text: systemPrompt }
: undefined : undefined

View File

@ -59,7 +59,6 @@ import {
mcpToolsToGeminiTools mcpToolsToGeminiTools
} from '@renderer/utils/mcp-tools' } from '@renderer/utils/mcp-tools'
import { findFileBlocks, findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' import { findFileBlocks, findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
import { buildSystemPrompt } from '@renderer/utils/prompt'
import { defaultTimeout, MB } from '@shared/config/constant' import { defaultTimeout, MB } from '@shared/config/constant'
import { BaseApiClient } from '../BaseApiClient' import { BaseApiClient } from '../BaseApiClient'
@ -448,7 +447,7 @@ export class GeminiAPIClient extends BaseApiClient<
}> => { }> => {
const { messages, mcpTools, maxTokens, enableWebSearch, enableUrlContext, enableGenerateImage } = coreRequest const { messages, mcpTools, maxTokens, enableWebSearch, enableUrlContext, enableGenerateImage } = coreRequest
// 1. 处理系统消息 // 1. 处理系统消息
let systemInstruction = assistant.prompt const systemInstruction = assistant.prompt
// 2. 设置工具 // 2. 设置工具
const { tools } = this.setupToolsConfig({ const { tools } = this.setupToolsConfig({
@ -457,10 +456,6 @@ export class GeminiAPIClient extends BaseApiClient<
enableToolUse: isEnabledToolUse(assistant) enableToolUse: isEnabledToolUse(assistant)
}) })
if (this.useSystemPromptForTools) {
systemInstruction = await buildSystemPrompt(assistant.prompt || '', mcpTools, assistant)
}
let messageContents: Content = { role: 'user', parts: [] } // Initialize messageContents let messageContents: Content = { role: 'user', parts: [] } // Initialize messageContents
const history: Content[] = [] const history: Content[] = []
// 3. 处理用户消息 // 3. 处理用户消息

View File

@ -52,7 +52,6 @@ import {
openAIToolsToMcpTool openAIToolsToMcpTool
} from '@renderer/utils/mcp-tools' } from '@renderer/utils/mcp-tools'
import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find' import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find'
import { buildSystemPrompt } from '@renderer/utils/prompt'
import OpenAI, { AzureOpenAI } from 'openai' import OpenAI, { AzureOpenAI } from 'openai'
import { ChatCompletionContentPart, ChatCompletionContentPartRefusal, ChatCompletionTool } from 'openai/resources' import { ChatCompletionContentPart, ChatCompletionContentPartRefusal, ChatCompletionTool } from 'openai/resources'
@ -494,10 +493,6 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
enableToolUse: isEnabledToolUse(assistant) enableToolUse: isEnabledToolUse(assistant)
}) })
if (this.useSystemPromptForTools) {
systemMessage.content = await buildSystemPrompt(systemMessage.content || '', mcpTools, assistant)
}
// 3. 处理用户消息 // 3. 处理用户消息
const userMessages: OpenAISdkMessageParam[] = [] const userMessages: OpenAISdkMessageParam[] = []
if (typeof messages === 'string') { if (typeof messages === 'string') {

View File

@ -36,7 +36,6 @@ import {
openAIToolsToMcpTool openAIToolsToMcpTool
} from '@renderer/utils/mcp-tools' } from '@renderer/utils/mcp-tools'
import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find' import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find'
import { buildSystemPrompt } from '@renderer/utils/prompt'
import { MB } from '@shared/config/constant' import { MB } from '@shared/config/constant'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import OpenAI, { AzureOpenAI } from 'openai' import OpenAI, { AzureOpenAI } from 'openai'
@ -377,9 +376,6 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
enableToolUse: isEnabledToolUse(assistant) enableToolUse: isEnabledToolUse(assistant)
}) })
if (this.useSystemPromptForTools) {
systemMessageInput.text = await buildSystemPrompt(systemMessageInput.text || '', mcpTools, assistant)
}
systemMessageContent.push(systemMessageInput) systemMessageContent.push(systemMessageInput)
systemMessage.content = systemMessageContent systemMessage.content = systemMessageContent

View File

@ -3,6 +3,7 @@ import { MCPCallToolResponse, MCPTool, MCPToolResponse, Model, ToolCallResponse
import { ChunkType, MCPToolCreatedChunk } from '@renderer/types/chunk' import { ChunkType, MCPToolCreatedChunk } from '@renderer/types/chunk'
import { SdkMessageParam, SdkRawOutput, SdkToolCall } from '@renderer/types/sdk' import { SdkMessageParam, SdkRawOutput, SdkToolCall } from '@renderer/types/sdk'
import { import {
callBuiltInTool,
callMCPTool, callMCPTool,
getMcpServerByTool, getMcpServerByTool,
isToolAutoApproved, isToolAutoApproved,
@ -469,7 +470,10 @@ export async function parseAndCallTools<R>(
// 执行工具调用 // 执行工具调用
try { try {
const images: string[] = [] 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状态 // 立即更新为done状态
upsertMCPToolResponse( upsertMCPToolResponse(
@ -482,6 +486,10 @@ export async function parseAndCallTools<R>(
onChunk! onChunk!
) )
if (!toolCallResponse) {
return
}
// 处理图片 // 处理图片
for (const content of toolCallResponse.content) { for (const content of toolCallResponse.content) {
if (content.type === 'image' && content.data) { if (content.type === 'image' && content.data) {

View File

@ -4,6 +4,7 @@ export const DEFAULT_MAX_TOKENS = 4096
export const DEFAULT_KNOWLEDGE_DOCUMENT_COUNT = 6 export const DEFAULT_KNOWLEDGE_DOCUMENT_COUNT = 6
export const DEFAULT_KNOWLEDGE_THRESHOLD = 0.0 export const DEFAULT_KNOWLEDGE_THRESHOLD = 0.0
export const DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT = 1 export const DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT = 1
export const SYSTEM_PROMPT_THRESHOLD = 128
export const platform = window.electron?.process?.platform export const platform = window.electron?.process?.platform
export const isMac = platform === 'darwin' export const isMac = platform === 'darwin'

View File

@ -335,7 +335,6 @@
"provider": "Παρέχων", "provider": "Παρέχων",
"reasoning_content": "Έχει σκεφτεί πολύ καλά", "reasoning_content": "Έχει σκεφτεί πολύ καλά",
"regenerate": "Ξαναπαραγωγή", "regenerate": "Ξαναπαραγωγή",
"trace": "ίχνος",
"rename": "Μετονομασία", "rename": "Μετονομασία",
"reset": "Επαναφορά", "reset": "Επαναφορά",
"save": "Αποθήκευση", "save": "Αποθήκευση",
@ -347,6 +346,7 @@
"pinyin.desc": "Φθίνουσα ταξινόμηση κατά Πινγίν" "pinyin.desc": "Φθίνουσα ταξινόμηση κατά Πινγίν"
}, },
"topics": "Θέματα", "topics": "Θέματα",
"trace": "ίχνος",
"warning": "Προσοχή", "warning": "Προσοχή",
"you": "Εσείς" "you": "Εσείς"
}, },

View File

@ -336,7 +336,6 @@
"provider": "Proveedor", "provider": "Proveedor",
"reasoning_content": "Pensamiento profundo", "reasoning_content": "Pensamiento profundo",
"regenerate": "Regenerar", "regenerate": "Regenerar",
"trace": "Rastro",
"rename": "Renombrar", "rename": "Renombrar",
"reset": "Restablecer", "reset": "Restablecer",
"save": "Guardar", "save": "Guardar",
@ -348,6 +347,7 @@
"pinyin.desc": "Ordenar por pinyin descendente" "pinyin.desc": "Ordenar por pinyin descendente"
}, },
"topics": "Temas", "topics": "Temas",
"trace": "Rastro",
"warning": "Advertencia", "warning": "Advertencia",
"you": "Usuario" "you": "Usuario"
}, },

View File

@ -335,7 +335,6 @@
"provider": "Fournisseur", "provider": "Fournisseur",
"reasoning_content": "Réflexion approfondie", "reasoning_content": "Réflexion approfondie",
"regenerate": "Regénérer", "regenerate": "Regénérer",
"trace": "Tracer",
"rename": "Renommer", "rename": "Renommer",
"reset": "Réinitialiser", "reset": "Réinitialiser",
"save": "Enregistrer", "save": "Enregistrer",
@ -347,6 +346,7 @@
"pinyin.desc": "Сортировать по пиньинь в порядке убывания" "pinyin.desc": "Сортировать по пиньинь в порядке убывания"
}, },
"topics": "Sujets", "topics": "Sujets",
"trace": "Tracer",
"warning": "Avertissement", "warning": "Avertissement",
"you": "Vous" "you": "Vous"
}, },

View File

@ -337,7 +337,6 @@
"provider": "Fornecedor", "provider": "Fornecedor",
"reasoning_content": "Pensamento profundo concluído", "reasoning_content": "Pensamento profundo concluído",
"regenerate": "Regenerar", "regenerate": "Regenerar",
"trace": "Regenerar",
"rename": "Renomear", "rename": "Renomear",
"reset": "Redefinir", "reset": "Redefinir",
"save": "Salvar", "save": "Salvar",
@ -349,6 +348,7 @@
"pinyin.desc": "Ordenar por Pinyin em ordem decrescente" "pinyin.desc": "Ordenar por Pinyin em ordem decrescente"
}, },
"topics": "Tópicos", "topics": "Tópicos",
"trace": "Regenerar",
"warning": "Aviso", "warning": "Aviso",
"you": "Você" "you": "Você"
}, },

View File

@ -1,5 +1,6 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { CompletionsParams } from '@renderer/aiCore/middleware/schemas' import { CompletionsParams } from '@renderer/aiCore/middleware/schemas'
import { SYSTEM_PROMPT_THRESHOLD } from '@renderer/config/constant'
import { import {
isEmbeddingModel, isEmbeddingModel,
isGenerateImageModel, isGenerateImageModel,
@ -39,6 +40,7 @@ import { removeSpecialCharactersForTopicName } from '@renderer/utils'
import { isAbortError } from '@renderer/utils/error' import { isAbortError } from '@renderer/utils/error'
import { extractInfoFromXML, ExtractResults } from '@renderer/utils/extract' import { extractInfoFromXML, ExtractResults } from '@renderer/utils/extract'
import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
import { buildSystemPromptWithThinkTool, buildSystemPromptWithTools } from '@renderer/utils/prompt'
import { findLast, isEmpty, takeRight } from 'lodash' import { findLast, isEmpty, takeRight } from 'lodash'
import AiProvider from '../aiCore' import AiProvider from '../aiCore'
@ -345,7 +347,7 @@ async function fetchExternalTool(
} }
// Get MCP tools (Fix duplicate declaration) // Get MCP tools (Fix duplicate declaration)
let mcpTools: MCPTool[] = [] // Initialize as empty array let mcpTools: MCPTool[] = []
const allMcpServers = store.getState().mcp.servers || [] const allMcpServers = store.getState().mcp.servers || []
const activedMcpServers = allMcpServers.filter((s) => s.isActive) const activedMcpServers = allMcpServers.filter((s) => s.isActive)
const assistantMcpServers = assistant.mcpServers || [] const assistantMcpServers = assistant.mcpServers || []
@ -369,6 +371,19 @@ async function fetchExternalTool(
.filter((result): result is PromiseFulfilledResult<MCPTool[]> => result.status === 'fulfilled') .filter((result): result is PromiseFulfilledResult<MCPTool[]> => result.status === 'fulfilled')
.map((result) => result.value) .map((result) => result.value)
.flat() .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) { } catch (toolError) {
logger.error('Error fetching MCP tools:', toolError as Error) logger.error('Error fetching MCP tools:', toolError as Error)
} }

View File

@ -17,6 +17,7 @@ import {
createTranslationBlock, createTranslationBlock,
resetAssistantMessage resetAssistantMessage
} from '@renderer/utils/messageUtils/create' } from '@renderer/utils/messageUtils/create'
import { buildSystemPrompt } from '@renderer/utils/prompt'
import { getTopicQueue } from '@renderer/utils/queue' import { getTopicQueue } from '@renderer/utils/queue'
import { waitForTopicQueue } from '@renderer/utils/queue' import { waitForTopicQueue } from '@renderer/utils/queue'
import { t } from 'i18next' import { t } from 'i18next'
@ -877,6 +878,8 @@ const fetchAndProcessAssistantResponseImpl = async (
// } // }
// } // }
assistant.prompt = await buildSystemPrompt(assistant.prompt || '', assistant)
callbacks = createCallbacks({ callbacks = createCallbacks({
blockManager, blockManager,
dispatch, dispatch,

View File

@ -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'

View File

@ -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.'
}
}
}
}

View File

@ -672,6 +672,7 @@ export interface MCPTool {
description?: string description?: string
inputSchema: MCPToolInputSchema inputSchema: MCPToolInputSchema
outputSchema?: z.infer<typeof MCPToolOutputSchema> outputSchema?: z.infer<typeof MCPToolOutputSchema>
isBuiltIn?: boolean // 标识是否为内置工具内置工具不需要通过MCP协议调用
} }
export interface MCPPromptArguments { export interface MCPPromptArguments {

View File

@ -1,67 +1,260 @@
import { type MCPTool } from '@renderer/types' import { configureStore } from '@reduxjs/toolkit'
import { describe, expect, it } from 'vitest' 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('prompt', () => {
describe('AvailableTools', () => { const mockDate = new Date('2024-01-01T12:00:00Z')
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)
expect(result).toContain('<tools>') beforeEach(() => {
expect(result).toContain('</tools>') // 重置所有 mocks
expect(result).toContain('<tool>') vi.clearAllMocks()
expect(result).toContain('test-tool') vi.useFakeTimers()
expect(result).toContain('Test tool description') vi.setSystemTime(mockDate)
expect(result).toContain('{"type":"object"}')
// 设置默认的 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 = `<tools>
<tool>
<name>test-tool</name>
<description>Test tool description</description>
<arguments>
{"type":"object","title":"test-tool-schema","properties":{}}
</arguments>
</tool>
</tools>`
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 result = AvailableTools([])
const expectedXml = `<tools>
expect(result).toContain('<tools>') </tools>`
expect(result).toContain('</tools>') expect(result).toEqual(expectedXml)
expect(result).not.toContain('<tool>')
}) })
}) })
describe('buildSystemPrompt', () => { describe('buildSystemPrompt', () => {
it('should build prompt with tools', async () => { it('should replace all variables correctly with strict equality', async () => {
const userPrompt = 'Custom user system prompt' const userPrompt = `
const tools = [ :
{ id: 'test-tool', description: 'Test tool description', inputSchema: { type: 'object' } } as MCPTool - : {{datetime}};
] - : {{system}};
const result = await buildSystemPrompt(userPrompt, tools) - : {{arch}};
- : {{language}};
expect(result).toContain(userPrompt) - : {{model_name}};
expect(result).toContain('test-tool') - : {{username}};
expect(result).toContain('Test tool description') `
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 () => { it('should handle API errors gracefully and use fallback values', async () => {
const userPrompt = 'Custom user system prompt' mockApi.system.getDeviceType.mockRejectedValue(new Error('API Error'))
const result = await buildSystemPrompt(userPrompt, []) 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 () => { it('should handle non-string input gracefully', async () => {
const tools = [ const result = await buildSystemPrompt(null as any)
{ id: 'test-tool', description: 'Test tool description', inputSchema: { type: 'object' } } as MCPTool expect(result).toBe(null)
] })
})
// 测试 userPrompt 为 null 的情况 describe('Tool prompt composition', () => {
const resultNull = buildSystemPrompt(null as any, tools) let basePrompt: string
expect(resultNull).toBeDefined() let expectedBasePrompt: string
expect(resultNull).not.toContain('{{ USER_SYSTEM_PROMPT }}') let tools: MCPTool[]
// 测试 userPrompt 为 undefined 的情况 beforeEach(async () => {
const resultUndefined = buildSystemPrompt(undefined as any, tools) const initialPrompt = `
expect(resultUndefined).toBeDefined() System Information:
expect(resultUndefined).not.toContain('{{ USER_SYSTEM_PROMPT }}') - 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') // 验证包含思考指令
}) })
}) })
}) })

View File

@ -266,6 +266,25 @@ export function openAIToolsToMcpTool(
return tool return tool
} }
export async function callBuiltInTool(toolResponse: MCPToolResponse): Promise<MCPCallToolResponse | undefined> {
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( export async function callMCPTool(
toolResponse: MCPToolResponse, toolResponse: MCPToolResponse,
topicId?: string, topicId?: string,

View File

@ -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. \ 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. 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:
<think_tool_example_1>
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
</think_tool_example_1>
<think_tool_example_2>
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
</think_tool_example_2>
## Tool Use Formatting ## 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: 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 Instructions
{{ USER_SYSTEM_PROMPT }} {{ 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. 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:
<think_tool_example_1>
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
</think_tool_example_1>
<think_tool_example_2>
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
</think_tool_example_2>
`
export const ToolUseExamples = ` export const ToolUseExamples = `
Here are a few examples using notional tools: Here are a few examples using notional tools:
--- ---
@ -151,11 +237,7 @@ ${availableTools}
</tools>` </tools>`
} }
export const buildSystemPrompt = async ( export const buildSystemPrompt = async (userSystemPrompt: string, assistant?: Assistant): Promise<string> => {
userSystemPrompt: string,
tools?: MCPTool[],
assistant?: Assistant
): Promise<string> => {
if (typeof userSystemPrompt === 'string') { if (typeof userSystemPrompt === 'string') {
const now = new Date() const now = new Date()
if (userSystemPrompt.includes('{{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) { 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('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples)
.replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools)) .replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools))
} }
return userSystemPrompt return userSystemPrompt
} }
export const buildSystemPromptWithThinkTool = (userSystemPrompt: string): string => {
return THINK_TOOL_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt || '')
}

View File

@ -1,9 +1,10 @@
<!DOCTYPE html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<style> <style>
html, body { html,
body {
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -26,7 +27,7 @@
flex-shrink: 0; flex-shrink: 0;
line-height: 36px; line-height: 36px;
position: relative; position: relative;
z-index: 1 z-index: 1;
} }
footer p { footer p {
margin: 0; margin: 0;
@ -37,4 +38,4 @@
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/trace/traceWindow.tsx"></script> <script type="module" src="/src/trace/traceWindow.tsx"></script>
</body> </body>
</html> </html>