diff --git a/src/main/presenter/builtInToolsPresenter/listFilesTool.ts b/src/main/presenter/builtInToolsPresenter/listFilesTool.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0cf87c900ec241ba7a142210b063025ca49625c --- /dev/null +++ b/src/main/presenter/builtInToolsPresenter/listFilesTool.ts @@ -0,0 +1,124 @@ +import fs from 'fs/promises' +import path from 'path' +import { BuiltInToolDefinition, BuiltInToolResponse, buildRawData } from './base' + +export const listFilesTool: BuiltInToolDefinition = { + name: 'list_files', + description: '列出指定目录中的文件和目录', + parameters: { + type: 'object', + properties: { + directory_path: { + type: 'string', + description: '要列出内容的目录路径' + }, + recursive: { + type: 'boolean', + description: '是否递归列出子目录', + default: false + } + }, + required: ['directory_path'] + } +} + +export async function executeListFilesTool( + args: any, + toolCallId: string +): Promise { + try { + const { directory_path, recursive = false } = args + + if (!directory_path) { + throw new Error('目录路径不能为空') + } + + const resolvedPath = path.isAbsolute(directory_path) + ? directory_path + : path.resolve(process.cwd(), directory_path) + + try { + await fs.access(resolvedPath) + } catch { + throw new Error(`目录不存在: ${resolvedPath}`) + } + + const stats = await fs.stat(resolvedPath) + if (!stats.isDirectory()) { + throw new Error(`路径指向的是文件而不是目录: ${resolvedPath}`) + } + + interface FileItemInfo { + name: string + path: string + type: 'directory' | 'file' + size: number + modified: Date + isDirectory: boolean + isFile: boolean + } + + const listFilesRecursive = async (dir: string): Promise => { + const items = await fs.readdir(dir, { withFileTypes: true }) + const result: FileItemInfo[] = [] + + for (const item of items) { + const fullPath = path.join(dir, item.name) + const itemStats = await fs.stat(fullPath) + + const itemInfo: FileItemInfo = { + name: item.name, + path: fullPath, + type: item.isDirectory() ? 'directory' : 'file', + size: itemStats.size, + modified: itemStats.mtime, + isDirectory: item.isDirectory(), + isFile: item.isFile() + } + + result.push(itemInfo) + + if (recursive && item.isDirectory()) { + const subItems = await listFilesRecursive(fullPath) + result.push(...subItems) + } + } + + return result + } + + const files = await listFilesRecursive(resolvedPath) + + const listMetadata = { + path: resolvedPath, + recursive, + totalItems: files.length, + items: files + } + const successContent = `目录内容列出成功:\n路径: ${resolvedPath}\n递归: ${recursive}\n\n找到 ${files.length} 个项目:\n${files + .map( + (item) => + `- ${item.type === 'directory' ? '📁' : '📄'} ${item.name} (${item.type}, ${item.size} bytes)` + ) + .join('\n')}` + + return { + toolCallId, + content: successContent, + success: true, + metadata: listMetadata, + rawData: buildRawData(toolCallId, successContent, false, listMetadata) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + const failureMessage = `列出文件失败: ${errorMessage}` + const metadata = { error: errorMessage } + return { + toolCallId, + content: failureMessage, + success: false, + metadata, + rawData: buildRawData(toolCallId, failureMessage, true, metadata) + } + } +} diff --git a/src/main/presenter/builtInToolsPresenter/readFileTool.ts b/src/main/presenter/builtInToolsPresenter/readFileTool.ts new file mode 100644 index 0000000000000000000000000000000000000000..bdbe07d652e46ffa0b068e7ae1227e5882ddb1d6 --- /dev/null +++ b/src/main/presenter/builtInToolsPresenter/readFileTool.ts @@ -0,0 +1,90 @@ +import fs from 'fs/promises' +import path from 'path' +import { BuiltInToolDefinition, BuiltInToolResponse, buildRawData } from './base' + +export const readFileTool: BuiltInToolDefinition = { + name: 'read_file', + description: '读取指定路径的文件内容', + parameters: { + type: 'object', + properties: { + file_path: { + type: 'string', + description: '要读取的文件路径,可以是绝对路径或相对路径' + }, + encoding: { + type: 'string', + description: '文件编码格式,默认为 utf-8', + enum: ['utf-8', 'base64', 'hex'], + default: 'utf-8' + } + }, + required: ['file_path'] + } +} + +export async function executeReadFileTool( + args: any, + toolCallId: string +): Promise { + try { + const { file_path, encoding = 'utf-8' } = args + + if (!file_path) { + throw new Error('文件路径不能为空') + } + + const resolvedPath = path.isAbsolute(file_path) + ? file_path + : path.resolve(process.cwd(), file_path) + + try { + await fs.access(resolvedPath) + } catch { + throw new Error(`文件不存在: ${resolvedPath}`) + } + + const stats = await fs.stat(resolvedPath) + if (stats.isDirectory()) { + throw new Error(`路径指向的是目录而不是文件: ${resolvedPath}`) + } + + let content: string + if (encoding === 'base64') { + const buffer = await fs.readFile(resolvedPath) + content = buffer.toString('base64') + } else if (encoding === 'hex') { + const buffer = await fs.readFile(resolvedPath) + content = buffer.toString('hex') + } else { + content = await fs.readFile(resolvedPath, 'utf-8') + } + + const fileInfo = { + path: resolvedPath, + size: stats.size, + modified: stats.mtime, + encoding + } + + const successMessage = `文件读取成功:\n路径: ${fileInfo.path}\n大小: ${fileInfo.size} bytes\n修改时间: ${fileInfo.modified}\n编码: ${fileInfo.encoding}\n\n文件内容:\n${content}` + return { + toolCallId, + content: successMessage, + success: true, + metadata: fileInfo, + rawData: buildRawData(toolCallId, successMessage, false, fileInfo) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + const failureMessage = `读取文件失败: ${errorMessage}` + const metadata = { error: errorMessage } + return { + toolCallId, + content: failureMessage, + success: false, + metadata, + rawData: buildRawData(toolCallId, failureMessage, true, metadata) + } + } +} diff --git a/src/main/presenter/builtInToolsPresenter/writeFileTool.ts b/src/main/presenter/builtInToolsPresenter/writeFileTool.ts new file mode 100644 index 0000000000000000000000000000000000000000..5aa13cfc471beb8b369f2dc50cf2cc669a077216 --- /dev/null +++ b/src/main/presenter/builtInToolsPresenter/writeFileTool.ts @@ -0,0 +1,85 @@ +import fs from 'fs/promises' +import path from 'path' +import { BuiltInToolDefinition, BuiltInToolResponse, buildRawData } from './base' + +export const writeFileTool: BuiltInToolDefinition = { + name: 'write_file', + description: '将内容写入指定路径的文件', + parameters: { + type: 'object', + properties: { + file_path: { + type: 'string', + description: '要写入的文件路径,可以是绝对路径或相对路径' + }, + content: { + type: 'string', + description: '要写入的文件内容' + }, + encoding: { + type: 'string', + description: '文件编码格式,默认为 utf-8', + enum: ['utf-8', 'base64'], + default: 'utf-8' + } + }, + required: ['file_path', 'content'] + } +} + +export async function executeWriteFileTool( + args: any, + toolCallId: string +): Promise { + try { + const { file_path, content, encoding = 'utf-8' } = args + + if (!file_path) { + throw new Error('文件路径不能为空') + } + + if (content === undefined || content === null) { + throw new Error('文件内容不能为空') + } + + const resolvedPath = path.isAbsolute(file_path) + ? file_path + : path.resolve(process.cwd(), file_path) + + const dir = path.dirname(resolvedPath) + await fs.mkdir(dir, { recursive: true }) + + if (encoding === 'base64') { + const buffer = Buffer.from(content, 'base64') + await fs.writeFile(resolvedPath, buffer) + } else { + await fs.writeFile(resolvedPath, content, 'utf-8') + } + + const fileInfo = { + path: resolvedPath, + encoding, + writtenAt: new Date().toISOString() + } + + const successMessage = `文件写入成功:\n路径: ${fileInfo.path}\n编码: ${fileInfo.encoding}\n写入时间: ${fileInfo.writtenAt}` + return { + toolCallId, + content: successMessage, + success: true, + metadata: fileInfo, + rawData: buildRawData(toolCallId, successMessage, false, fileInfo) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + const failureMessage = `写入文件失败: ${errorMessage}` + const metadata = { error: errorMessage } + return { + toolCallId, + content: failureMessage, + success: false, + metadata, + rawData: buildRawData(toolCallId, failureMessage, true, metadata) + } + } +} diff --git a/src/main/presenter/promptPresenter/sections/index.ts b/src/main/presenter/promptPresenter/sections/index.ts index e87c8b44b68e19b18b57e749df0a2ad7b5196d4e..88d77eb1c86a66b3ae1d66262e5233f3083d1755 100644 --- a/src/main/presenter/promptPresenter/sections/index.ts +++ b/src/main/presenter/promptPresenter/sections/index.ts @@ -2,5 +2,6 @@ export { getSystemInfoSection } from './system-info' export { getObjectiveSection } from './objective' export { addCustomInstructions } from './custom-instructions' export { getSharedToolUseSection } from './tool-use' +export { getToolDescriptionsSection } from './tool-descriptions' export { getToolUseGuidelinesSection } from './tool-use-guidelines' export { markdownFormattingSection } from './markdown-formatting' diff --git a/src/main/presenter/promptPresenter/sections/markdown-formatting.ts b/src/main/presenter/promptPresenter/sections/markdown-formatting.ts index ec102f2f41cd8b83e0c0dad5e0c9f2ff434c4009..eeb60e64039dc585d578130378959a741029877e 100644 --- a/src/main/presenter/promptPresenter/sections/markdown-formatting.ts +++ b/src/main/presenter/promptPresenter/sections/markdown-formatting.ts @@ -3,5 +3,5 @@ export function markdownFormattingSection(): string { MARKDOWN RULES -ALL responses MUST show ANY \`language construct\` OR filename reference as clickable, exactly as [\`filename OR language.declaration()\`](relative/file/path.ext:line); line is required for \`syntax\` and optional for filename links. This applies to ALL markdown responses and ALSO those in ` +ALL responses MUST show ANY \`language construct\` OR filename reference as clickable, exactly as [\`filename OR language.declaration()\`](relative/file/path.ext:line); line is required for \`syntax\` and optional for filename links. This applies to ALL markdown responses` } diff --git a/src/main/presenter/promptPresenter/sections/objective.ts b/src/main/presenter/promptPresenter/sections/objective.ts index 7608eb5d63b930a2cb527279d760a06c5722635f..36e3eab92d645ac57d117024a130c98e3b1cdb00 100644 --- a/src/main/presenter/promptPresenter/sections/objective.ts +++ b/src/main/presenter/promptPresenter/sections/objective.ts @@ -1,6 +1,4 @@ export function getObjectiveSection(): string { - const codebaseSearchInstruction = 'First, ' - return `==== OBJECTIVE @@ -9,7 +7,10 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order. 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. -3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. ${codebaseSearchInstruction}analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. -4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.` +3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead. DO NOT ask for more information on optional parameters if it is not provided. +4. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.` } + +// TODO: add attempt_completion and ask_followup_question tool +// 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. +// ask the user to provide the missing parameters using the ask_followup_question tool diff --git a/src/main/presenter/promptPresenter/sections/system-info.ts b/src/main/presenter/promptPresenter/sections/system-info.ts index 394b3890e3a06019bc42bc076c421fa5310a8e85..61f6c5e62870b25170d8e47b6b31283c883c0da9 100644 --- a/src/main/presenter/promptPresenter/sections/system-info.ts +++ b/src/main/presenter/promptPresenter/sections/system-info.ts @@ -1,16 +1,13 @@ import os from 'os' import osName from 'os-name' -export function getSystemInfoSection(cwd: string): string { - let details = `==== -f +export function getSystemInfoSection(): string { + return `==== + SYSTEM INFORMATION Operating System: ${osName()} System Arch: ${os.arch()} Home Directory: ${os.homedir()} -Current Workspace Directory: ${cwd} - ` - return details } diff --git a/src/main/presenter/promptPresenter/sections/tool-descriptions.ts b/src/main/presenter/promptPresenter/sections/tool-descriptions.ts new file mode 100644 index 0000000000000000000000000000000000000000..513cffba449d7eab00aa7d7ca3bb009d18b6c704 --- /dev/null +++ b/src/main/presenter/promptPresenter/sections/tool-descriptions.ts @@ -0,0 +1,109 @@ +export function getToolDescriptionsSection(): string { + return `## Tool Descriptions + +## read_file +描述:请求读取指定路径文件的内容。当您需要检查一个您不知道内容的现有文件时使用此工具,例如分析代码、查看文本文件或从配置文件中提取信息。输出内容会在每行前添加行号(例如:"1 | const x = 1"),这样在创建差异或讨论代码时更容易引用特定行。可以自动从PDF和DOCX文件中提取原始文本。可能不适用于其他类型的二进制文件,因为它会将原始内容作为字符串返回。 +参数: +- file_path:(必需)要读取的文件路径(当路径不确定时,向用户确认) +用法: + + +在此处填写文件路径 + + + +示例:请求读取frontend-config.json文件 + + +frontend-config.json + + + +## write_file +描述:请求将完整内容写入指定路径的文件。如果文件已存在,将用提供的内容覆盖它。如果文件不存在,将创建新文件。此工具会自动创建写入文件所需的所有目录。 +参数: +- file_path: (必需)要写入的文件路径(当路径不确定时,向用户确认) +- content: (必需)要写入文件的内容。始终提供文件的完整预期内容,不要有任何截断或遗漏。您必须包含文件的所有部分,即使它们没有被修改。但不要在内容中包含行号,只需提供文件的实际内容。 +- line_count: (必需)文件中的行数。确保根据文件的实际内容计算行数,而不是根据您提供的内容中的行数计算。 +用法: + + +在此处填写文件路径 + +在此处填写文件内容 + +文件的总行数,包括空行 + + + +示例:请求写入 frontend-config.json + + +frontend-config.json + +{ + "apiEndpoint": "https://api.example.com", + "theme": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + "fontFamily": "Arial, sans-serif" + }, + "features": { + "darkMode": true, + "notifications": true, + "analytics": false + }, + "version": "1.0.0" +} + +14 + + + +## list_files +描述:请求列出指定目录中的文件和子目录名称。当你需要了解目录结构或确认文件是否存在时使用此工具。默认只列出当前层级,必要时可以开启递归。 +参数: +- directory_path: (必需)要列出的目录路径(当路径不确定时,向用户确认) +- recursive: (可选)是否递归列出子目录, boolean值类型, 默认为 false。只有在确实需要完整目录树时才设为true,以避免输出过多信息。 +用法: + + +在此处填写目录路径 +false + + + +示例:请求列出 src/main 目录 + + +src/main +false + + + +## execute_command +描述:请求执行命令行指令, 并返回标准输出和标准错误。只有当需要运行系统本身相关的命令,可以使用此工具,但网络请求或访问外部网站,不可使用此工具,即仅在有明确需求时使用,注意评估命令可能带来的副作用。您必须根据用户的系统定制命令,并清楚地解释命令的功能。对于命令链接,请使用适合用户shell的链接语法。默认在当前工作目录运行,可按需指定其它目录或 shell。 +参数: +- command: (必需)要执行的完整命令字符串。 +- working_directory: (可选)命令执行时使用的目录,当路径必须时,向用户确认。 +- timeout: (可选)命令允许运行的最长时间(毫秒),number值类型, 留空默认 30000。 +- shell: (可选)用于执行命令的 shell,例如 \`powershell.exe\`、\`bash\`,留空使用系统默认值。 +用法: + + +在此处填写命令 +可选工作目录 +30000 +可选 shell + + + +示例:查看 Node.js 版本 + + +node -v +5000 + + +` +} diff --git a/src/main/presenter/promptPresenter/sections/tool-use-guidelines.ts b/src/main/presenter/promptPresenter/sections/tool-use-guidelines.ts index 76f52815fc8bf417aef35c6858fd812c4ec182b5..3e18fa02b03ba8f7b07bc6804c6942f76bb123fc 100644 --- a/src/main/presenter/promptPresenter/sections/tool-use-guidelines.ts +++ b/src/main/presenter/promptPresenter/sections/tool-use-guidelines.ts @@ -1,34 +1,12 @@ export function getToolUseGuidelinesSection(): string { - let itemNumber = 1 - const guidelinesList: string[] = [] + return `## Tool Use Guidelines - guidelinesList.push( - `${itemNumber++}. Analyze user intent, determine whether to call the tool based on its description, and select the most matching tool or tools.` - ) - - // First guideline is always the same - guidelinesList.push( - `${itemNumber++}. Assess what information you already have ,and proactively and clearly inquire about missing necessary parameters to ensure that the format of the call request fully complies with the tool interface specifications.` - ) - - // Remaining guidelines - guidelinesList.push( - `${itemNumber++}. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Each step must be informed by the previous step's result.` - ) - guidelinesList.push( - `${itemNumber++}. Formulate your tool use using the XML format specified for each tool.` - ) - guidelinesList.push( - `${itemNumber++}. Properly handle any errors in tool calls, convert them into user-friendly prompts that users can understand, and do not expose technical details.` - ) - guidelinesList.push( - `${itemNumber++}. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.` - ) - - // Join guidelines and add the footer - return `# Tool Use Guidelines - -${guidelinesList.join('\n')} +1. Analyze user intent, determine whether to call the tool based on its description, and select the most matching tool or tools. +2. Assess what information you already have ,and proactively and clearly inquire about missing necessary parameters to ensure that the format of the call request fully complies with the tool interface specifications. +3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Each step must be informed by the previous step's result. +4. Formulate your tool use using the XML format specified for each tool. +5. Properly handle any errors in tool calls, convert them into user-friendly prompts that users can understand, and do not expose technical details. +6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user. It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to: 1. Confirm the success of each step before proceeding. diff --git a/src/main/presenter/promptPresenter/sections/tool-use.ts b/src/main/presenter/promptPresenter/sections/tool-use.ts index aa6ae2ff623d3c02a4bfd3c6f235e4415cd294f8..a3f43a2b4a9ff54f60aed3bdd8c5352a10638596 100644 --- a/src/main/presenter/promptPresenter/sections/tool-use.ts +++ b/src/main/presenter/promptPresenter/sections/tool-use.ts @@ -1,19 +1,24 @@ export function getSharedToolUseSection(): string { return `==== -TOOL USE +# TOOL USE +- Tool Use Formatting +- Tool Descriptions +- Tool Use Guidelines -You have access to a set of tools that are executed upon the user's approval. You can use one tool 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. +Tools should be called only when necessary, only to solve direct problems, only system operations, command line execution related, other problems priority to use knowledge base or other methods to solve. +You have access to a set of tools that are executed upon the user's approval, you can use one tool 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. -# Tool Use Formatting +## Tool Use Formatting Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure: - + value1 value2 ... + Always use the actual tool name as the XML tag name for proper parsing and execution.` } diff --git a/src/main/presenter/promptPresenter/system.ts b/src/main/presenter/promptPresenter/system.ts index ce45fa42a0e00f80c2872916cf8b9e1c49fae7c0..25e97d4179698a3490f98f7c30ede9ebc4a54a69 100644 --- a/src/main/presenter/promptPresenter/system.ts +++ b/src/main/presenter/promptPresenter/system.ts @@ -6,6 +6,7 @@ import { getObjectiveSection, getSharedToolUseSection, getToolUseGuidelinesSection, + getToolDescriptionsSection, addCustomInstructions, markdownFormattingSection } from './sections' @@ -34,11 +35,12 @@ async function generatePrompt( ${markdownFormattingSection()} ${getSharedToolUseSection()} - )} + + ${getToolDescriptionsSection()} ${getToolUseGuidelinesSection()} - ${getSystemInfoSection(cwd || '')} + ${getSystemInfoSection()} ${getObjectiveSection()}