From 10496a409a36250f6b7a6f4df86b0a708fee0021 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Sat, 9 Nov 2024 21:04:03 +0100 Subject: [PATCH 1/4] Improve Workspace agent functions and prompt fixed #14361 Signed-off-by: Jonas Helming --- .../src/browser/frontend-module.ts | 3 +- .../src/browser/functions.ts | 166 ++++++++++++++---- .../src/common/functions.ts | 1 + .../ai-workspace-agent/src/common/template.ts | 50 ++---- 4 files changed, 145 insertions(+), 75 deletions(-) diff --git a/packages/ai-workspace-agent/src/browser/frontend-module.ts b/packages/ai-workspace-agent/src/browser/frontend-module.ts index 101f3702b0cce..b4adcd4da9d79 100644 --- a/packages/ai-workspace-agent/src/browser/frontend-module.ts +++ b/packages/ai-workspace-agent/src/browser/frontend-module.ts @@ -17,7 +17,7 @@ import { ContainerModule } from '@theia/core/shared/inversify'; import { ChatAgent } from '@theia/ai-chat/lib/common'; import { Agent, ToolProvider } from '@theia/ai-core/lib/common'; import { WorkspaceAgent } from './workspace-agent'; -import { FileContentFunction, GetWorkspaceFileList } from './functions'; +import { FileContentFunction, GetWorkspaceDirectoryStructure, GetWorkspaceFileList } from './functions'; export default new ContainerModule(bind => { bind(WorkspaceAgent).toSelf().inSingletonScope(); @@ -25,4 +25,5 @@ export default new ContainerModule(bind => { bind(ChatAgent).toService(WorkspaceAgent); bind(ToolProvider).to(GetWorkspaceFileList); bind(ToolProvider).to(FileContentFunction); + bind(ToolProvider).to(GetWorkspaceDirectoryStructure); }); diff --git a/packages/ai-workspace-agent/src/browser/functions.ts b/packages/ai-workspace-agent/src/browser/functions.ts index d6b3a4fa6d68d..3a4353c6f27ac 100644 --- a/packages/ai-workspace-agent/src/browser/functions.ts +++ b/packages/ai-workspace-agent/src/browser/functions.ts @@ -19,11 +19,61 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileStat } from '@theia/filesystem/lib/common/files'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID } from '../common/functions'; +import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID } from '../common/functions'; + +function shouldExclude(stat: FileStat): boolean { + const excludedFolders = ['node_modules', 'lib']; + return stat.resource.path.base.startsWith('.') || excludedFolders.includes(stat.resource.path.base); +} + +@injectable() +export class GetWorkspaceDirectoryStructure implements ToolProvider { + static ID = GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID; + + getTool(): ToolRequest { + return { + id: GetWorkspaceDirectoryStructure.ID, + name: GetWorkspaceDirectoryStructure.ID, + description: 'Retrieve the complete directory structure of the workspace, listing only directories (no file contents). This structure excludes specific directories, such as node_modules and hidden files, ensuring paths are within workspace boundaries.', + handler: () => this.getDirectoryStructure() + }; + } + + @inject(WorkspaceService) + protected workspaceService: WorkspaceService; + + @inject(FileService) + protected readonly fileService: FileService; + + private async getDirectoryStructure(): Promise { + const wsRoots = await this.workspaceService.roots; + + if (wsRoots.length === 0) { + throw new Error('Workspace root not found'); + } + + const workspaceRootUri = wsRoots[0].resource; + + return this.buildDirectoryStructure(workspaceRootUri); + } + + private async buildDirectoryStructure(uri: URI, prefix: string = ''): Promise { + const stat = await this.fileService.resolve(uri); + const result: string[] = []; + + if (stat && stat.isDirectory && stat.children) { + for (const child of stat.children) { + if (!child.isDirectory || shouldExclude(child)) continue; + const path = `${prefix}${child.resource.path.base}/`; + result.push(path); + result.push(...await this.buildDirectoryStructure(child.resource, `${path}`)); + } + } + + return result; + } +} -/** - * A Function that can read the contents of a File from the Workspace. - */ @injectable() export class FileContentFunction implements ToolProvider { static ID = FILE_CONTENT_FUNCTION_ID; @@ -32,13 +82,13 @@ export class FileContentFunction implements ToolProvider { return { id: FileContentFunction.ID, name: FileContentFunction.ID, - description: 'Get the content of the file', + description: 'The relative path to the target file within the workspace. This path is resolved from the workspace root, and only files within the workspace boundaries are accessible. Attempting to access paths outside the workspace will result in an error.', parameters: { type: 'object', properties: { file: { type: 'string', - description: 'The path of the file to retrieve content for', + description: 'Return the content of a specified file within the workspace. The file path must be provided relative to the workspace root. Only files within workspace boundaries are accessible; attempting to access files outside the workspace will return an error.', } } }, @@ -61,15 +111,36 @@ export class FileContentFunction implements ToolProvider { } private async getFileContent(file: string): Promise { - const uri = new URI(file); - const fileContent = await this.fileService.read(uri); - return fileContent.value; + const wsRoots = await this.workspaceService.roots; + + if (wsRoots.length === 0) { + throw new Error('Workspace root not found'); + } + + const workspaceRootUri = wsRoots[0].resource; + + const targetUri = workspaceRootUri.resolve(file); + + if (!targetUri.toString().startsWith(workspaceRootUri.toString())) { + throw new Error('Access outside of the workspace is not allowed'); + } + + try { + const fileStat = await this.fileService.resolve(targetUri); + + if (!fileStat || fileStat.isDirectory) { + return JSON.stringify({ error: "File not found" }); + } + + const fileContent = await this.fileService.read(targetUri); + return fileContent.value; + + } catch (error) { + return JSON.stringify({ error: "File not found" }); + } } } -/** - * A Function that lists all files in the workspace. - */ @injectable() export class GetWorkspaceFileList implements ToolProvider { static ID = GET_WORKSPACE_FILE_LIST_FUNCTION_ID; @@ -78,9 +149,20 @@ export class GetWorkspaceFileList implements ToolProvider { return { id: GetWorkspaceFileList.ID, name: GetWorkspaceFileList.ID, - description: 'List all files in the workspace', - - handler: () => this.getProjectFileList() + parameters: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Optional relative path to a directory within the workspace. If no path is specified, the function lists contents directly in the workspace root. Paths are resolved within workspace boundaries only; paths outside the workspace or unvalidated paths will result in an error.' + } + } + }, + description: 'List files and directories within a specified workspace directory. Paths are relative to the workspace root, and only workspace-contained paths are allowed. If no path is provided, the root contents are listed. Paths outside the workspace will result in an error.', + handler: (arg_string: string) => { + const args = JSON.parse(arg_string); + return this.getProjectFileList(args.path); + } }; } @@ -90,45 +172,51 @@ export class GetWorkspaceFileList implements ToolProvider { @inject(FileService) protected readonly fileService: FileService; - async getProjectFileList(): Promise { - // Get all files from the workspace service as a flat list of qualified file names + async getProjectFileList(path?: string): Promise { const wsRoots = await this.workspaceService.roots; - const result: string[] = []; - for (const root of wsRoots) { - result.push(...await this.listFilesRecursively(root.resource)); + + if (wsRoots.length === 0) { + throw new Error('Workspace root not found'); + } + + const workspaceRootUri = wsRoots[0].resource; + const targetUri = path ? workspaceRootUri.resolve(path) : workspaceRootUri; + + if (!targetUri.toString().startsWith(workspaceRootUri.toString())) { + throw new Error('Access outside of the workspace is not allowed'); + } + + try { + const stat = await this.fileService.resolve(targetUri); + if (!stat || !stat.isDirectory) { + return ["Error: Directory not found"]; + } + return await this.listFilesDirectly(targetUri, workspaceRootUri); + + } catch (error) { + return ["Error: Directory not found"]; } - return result; } - private async listFilesRecursively(uri: URI): Promise { + private async listFilesDirectly(uri: URI, workspaceRootUri: URI): Promise { const stat = await this.fileService.resolve(uri); const result: string[] = []; + if (stat && stat.isDirectory) { - if (this.exclude(stat)) { + if (shouldExclude(stat)) { return result; } const children = await this.fileService.resolve(uri); if (children.children) { for (const child of children.children) { - result.push(child.resource.toString()); - result.push(...await this.listFilesRecursively(child.resource)); + const relativePath = workspaceRootUri.relative(child.resource); + if (relativePath) { + result.push(relativePath.toString()); + } } } } - return result; - } - // Exclude folders which are not relevant to the AI Agent - private exclude(stat: FileStat): boolean { - if (stat.resource.path.base.startsWith('.')) { - return true; - } - if (stat.resource.path.base === 'node_modules') { - return true; - } - if (stat.resource.path.base === 'lib') { - return true; - } - return false; + return result; } } diff --git a/packages/ai-workspace-agent/src/common/functions.ts b/packages/ai-workspace-agent/src/common/functions.ts index 852a6c8f60f95..5157daf461dae 100644 --- a/packages/ai-workspace-agent/src/common/functions.ts +++ b/packages/ai-workspace-agent/src/common/functions.ts @@ -15,3 +15,4 @@ // ***************************************************************************** export const FILE_CONTENT_FUNCTION_ID = 'getFileContent'; export const GET_WORKSPACE_FILE_LIST_FUNCTION_ID = 'getWorkspaceFileList'; +export const GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID = 'getWorkspaceDirectoryStructure'; diff --git a/packages/ai-workspace-agent/src/common/template.ts b/packages/ai-workspace-agent/src/common/template.ts index ec825d90a98df..4c53d31f7dba4 100644 --- a/packages/ai-workspace-agent/src/common/template.ts +++ b/packages/ai-workspace-agent/src/common/template.ts @@ -14,50 +14,30 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { PromptTemplate } from '@theia/ai-core/lib/common'; -import { GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FILE_CONTENT_FUNCTION_ID } from './functions'; +import { GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID } from './functions'; export const workspaceTemplate = { id: 'workspace-system', template: `# Instructions - You are an AI assistant integrated into the Theia IDE, specifically designed to help software developers by -providing concise and accurate answers to programming-related questions. Your role is to enhance the -developer's productivity by offering quick solutions, explanations, and best practices. -Keep responses short and to the point, focusing on delivering valuable insights, best practices and -simple solutions. -You are specialized in providing insights based on the Theia IDE's workspace and its files. -Use the following functions to access the workspace: -- ~{${GET_WORKSPACE_FILE_LIST_FUNCTION_ID}} -- ~{${FILE_CONTENT_FUNCTION_ID}}. Never shorten the file paths when using this function. +You are an AI assistant integrated into Theia IDE, designed to assist software developers with concise answers to programming-related questions. Your goal is to enhance productivity with quick, relevant solutions, explanations, and best practices. Keep responses short, delivering valuable insights and direct solutions. -## Guidelines +Use the following functions to interact with the workspace files as needed: +- **~{${GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID}}**: Returns the complete directory structure. +- **~{${GET_WORKSPACE_FILE_LIST_FUNCTION_ID}}**: Lists files and directories in a specific directory. +- **~{${FILE_CONTENT_FUNCTION_ID}}**: Retrieves the content of a specific file. -1. **Understand Context:** - - **Always answer in context of the workspace and its files. Avoid general answers**. - - Use the provided functions to access the workspace files. **Never assume the workspace structure or file contents.** - - Tailor responses to be relevant to the programming language, framework, or tools like Eclipse Theia used in the workspace. - - Ask clarifying questions if necessary to provide accurate assistance. Always assume it is okay to read additional files from the workspace. +### Workspace Navigation Guidelines -2. **Provide Clear Solutions:** - - Offer direct answers or code snippets that solve the problem or clarify the concept. - - Avoid lengthy explanations unless necessary for understanding. - - Provide links to official documentation for further reading when applicable. +1. **Confirm Paths**: Always verify paths by listing directories or files as you navigate. Avoid assumptions based on user input alone. +2. **Start from Root**: Begin at the root and navigate subdirectories step-by-step. -3. **Support Multiple Languages and Tools:** - - Be familiar with popular programming languages, frameworks, IDEs like Eclipse Theia, and command-line tools. - - Adapt advice based on the language, environment, or tools specified by the developer. +### Response Guidelines -4. **Facilitate Learning:** - - Encourage learning by explaining why a solution works or why a particular approach is recommended. - - Keep explanations concise and educational. - -5. **Maintain Professional Tone:** - - Communicate in a friendly, professional manner. - - Use technical jargon appropriately, ensuring clarity for the target audience. - -6. **Stay on Topic:** - - Limit responses strictly to topics related to software development, frameworks, Eclipse Theia, terminal usage, and relevant technologies. - - Politely decline to answer questions unrelated to these areas by saying, "I'm here to assist with programming-related questions. - For other topics, please refer to a specialized source." +1. **Contextual Focus**: Provide answers relevant to the workspace, avoiding general advice. Use provided functions without assuming file structure or content. +2. **Clear Solutions**: Offer direct answers and concise explanations. Link to official documentation as needed. +3. **Tool & Language Adaptability**: Adjust guidance based on the programming language, framework, or tool specified by the developer. +4. **Supportive Tone**: Maintain a friendly, professional tone with clear, accurate technical language. +5. **Stay Relevant**: Limit responses to software development, frameworks, Theia, terminal usage, and related technologies. Decline unrelated questions politely. ` }; From 930fb96075a9baabeee09b490adc7256ae463915 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Sat, 9 Nov 2024 22:37:01 +0100 Subject: [PATCH 2/4] Fix linting errors Signed-off-by: Jonas Helming --- .../src/browser/functions.ts | 25 +++++++++++-------- .../ai-workspace-agent/src/common/template.ts | 3 ++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/ai-workspace-agent/src/browser/functions.ts b/packages/ai-workspace-agent/src/browser/functions.ts index 3a4353c6f27ac..4e90830d34f41 100644 --- a/packages/ai-workspace-agent/src/browser/functions.ts +++ b/packages/ai-workspace-agent/src/browser/functions.ts @@ -34,7 +34,8 @@ export class GetWorkspaceDirectoryStructure implements ToolProvider { return { id: GetWorkspaceDirectoryStructure.ID, name: GetWorkspaceDirectoryStructure.ID, - description: 'Retrieve the complete directory structure of the workspace, listing only directories (no file contents). This structure excludes specific directories, such as node_modules and hidden files, ensuring paths are within workspace boundaries.', + description: `Retrieve the complete directory structure of the workspace, listing only directories (no file contents). This structure excludes specific directories, + such as node_modules and hidden files, ensuring paths are within workspace boundaries.`, handler: () => this.getDirectoryStructure() }; } @@ -63,7 +64,7 @@ export class GetWorkspaceDirectoryStructure implements ToolProvider { if (stat && stat.isDirectory && stat.children) { for (const child of stat.children) { - if (!child.isDirectory || shouldExclude(child)) continue; + if (!child.isDirectory || shouldExclude(child)) { continue; }; const path = `${prefix}${child.resource.path.base}/`; result.push(path); result.push(...await this.buildDirectoryStructure(child.resource, `${path}`)); @@ -82,13 +83,15 @@ export class FileContentFunction implements ToolProvider { return { id: FileContentFunction.ID, name: FileContentFunction.ID, - description: 'The relative path to the target file within the workspace. This path is resolved from the workspace root, and only files within the workspace boundaries are accessible. Attempting to access paths outside the workspace will result in an error.', + description: `The relative path to the target file within the workspace. This path is resolved from the workspace root, and only files within the workspace boundaries + are accessible. Attempting to access paths outside the workspace will result in an error.`, parameters: { type: 'object', properties: { file: { type: 'string', - description: 'Return the content of a specified file within the workspace. The file path must be provided relative to the workspace root. Only files within workspace boundaries are accessible; attempting to access files outside the workspace will return an error.', + description: `Return the content of a specified file within the workspace. The file path must be provided relative to the workspace root. Only files within + workspace boundaries are accessible; attempting to access files outside the workspace will return an error.`, } } }, @@ -129,14 +132,14 @@ export class FileContentFunction implements ToolProvider { const fileStat = await this.fileService.resolve(targetUri); if (!fileStat || fileStat.isDirectory) { - return JSON.stringify({ error: "File not found" }); + return JSON.stringify({ error: 'File not found' }); } const fileContent = await this.fileService.read(targetUri); return fileContent.value; } catch (error) { - return JSON.stringify({ error: "File not found" }); + return JSON.stringify({ error: 'File not found' }); } } } @@ -154,11 +157,13 @@ export class GetWorkspaceFileList implements ToolProvider { properties: { path: { type: 'string', - description: 'Optional relative path to a directory within the workspace. If no path is specified, the function lists contents directly in the workspace root. Paths are resolved within workspace boundaries only; paths outside the workspace or unvalidated paths will result in an error.' + description: `Optional relative path to a directory within the workspace. If no path is specified, the function lists contents directly in the workspace + root. Paths are resolved within workspace boundaries only; paths outside the workspace or unvalidated paths will result in an error.` } } }, - description: 'List files and directories within a specified workspace directory. Paths are relative to the workspace root, and only workspace-contained paths are allowed. If no path is provided, the root contents are listed. Paths outside the workspace will result in an error.', + description: `List files and directories within a specified workspace directory. Paths are relative to the workspace root, and only workspace-contained paths are + allowed. If no path is provided, the root contents are listed. Paths outside the workspace will result in an error.`, handler: (arg_string: string) => { const args = JSON.parse(arg_string); return this.getProjectFileList(args.path); @@ -189,12 +194,12 @@ export class GetWorkspaceFileList implements ToolProvider { try { const stat = await this.fileService.resolve(targetUri); if (!stat || !stat.isDirectory) { - return ["Error: Directory not found"]; + return ['Error: Directory not found']; } return await this.listFilesDirectly(targetUri, workspaceRootUri); } catch (error) { - return ["Error: Directory not found"]; + return ['Error: Directory not found']; } } diff --git a/packages/ai-workspace-agent/src/common/template.ts b/packages/ai-workspace-agent/src/common/template.ts index 4c53d31f7dba4..51897af1eca93 100644 --- a/packages/ai-workspace-agent/src/common/template.ts +++ b/packages/ai-workspace-agent/src/common/template.ts @@ -20,7 +20,8 @@ export const workspaceTemplate = { id: 'workspace-system', template: `# Instructions -You are an AI assistant integrated into Theia IDE, designed to assist software developers with concise answers to programming-related questions. Your goal is to enhance productivity with quick, relevant solutions, explanations, and best practices. Keep responses short, delivering valuable insights and direct solutions. +You are an AI assistant integrated into Theia IDE, designed to assist software developers with concise answers to programming-related questions. Your goal is to enhance +productivity with quick, relevant solutions, explanations, and best practices. Keep responses short, delivering valuable insights and direct solutions. Use the following functions to interact with the workspace files as needed: - **~{${GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID}}**: Returns the complete directory structure. From 589e416dab4d3393928818c4b93c9ed9e7112a4b Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Tue, 12 Nov 2024 00:16:17 +0100 Subject: [PATCH 3/4] Adressed review comments Signed-off-by: Jonas Helming --- .../src/browser/frontend-module.ts | 3 +- .../src/browser/functions.ts | 103 ++++++++++-------- .../ai-workspace-agent/src/common/template.ts | 5 +- 3 files changed, 65 insertions(+), 46 deletions(-) diff --git a/packages/ai-workspace-agent/src/browser/frontend-module.ts b/packages/ai-workspace-agent/src/browser/frontend-module.ts index b4adcd4da9d79..ef3f02174dc7a 100644 --- a/packages/ai-workspace-agent/src/browser/frontend-module.ts +++ b/packages/ai-workspace-agent/src/browser/frontend-module.ts @@ -17,7 +17,7 @@ import { ContainerModule } from '@theia/core/shared/inversify'; import { ChatAgent } from '@theia/ai-chat/lib/common'; import { Agent, ToolProvider } from '@theia/ai-core/lib/common'; import { WorkspaceAgent } from './workspace-agent'; -import { FileContentFunction, GetWorkspaceDirectoryStructure, GetWorkspaceFileList } from './functions'; +import { FileContentFunction, GetWorkspaceDirectoryStructure, GetWorkspaceFileList, WorkspaceUtils } from './functions'; export default new ContainerModule(bind => { bind(WorkspaceAgent).toSelf().inSingletonScope(); @@ -26,4 +26,5 @@ export default new ContainerModule(bind => { bind(ToolProvider).to(GetWorkspaceFileList); bind(ToolProvider).to(FileContentFunction); bind(ToolProvider).to(GetWorkspaceDirectoryStructure); + bind(WorkspaceUtils).toSelf().inSingletonScope(); }); diff --git a/packages/ai-workspace-agent/src/browser/functions.ts b/packages/ai-workspace-agent/src/browser/functions.ts index 4e90830d34f41..ee80cca9f675d 100644 --- a/packages/ai-workspace-agent/src/browser/functions.ts +++ b/packages/ai-workspace-agent/src/browser/functions.ts @@ -21,9 +21,34 @@ import { FileStat } from '@theia/filesystem/lib/common/files'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID } from '../common/functions'; -function shouldExclude(stat: FileStat): boolean { - const excludedFolders = ['node_modules', 'lib']; - return stat.resource.path.base.startsWith('.') || excludedFolders.includes(stat.resource.path.base); +@injectable() +export class WorkspaceUtils { + @inject(WorkspaceService) + protected workspaceService: WorkspaceService; + + async getWorkspaceRoot(): Promise { + const wsRoots = await this.workspaceService.roots; + if (wsRoots.length === 0) { + throw new Error('No workspace has been opened yet'); + } + return wsRoots[0].resource; + } + + ensureWithinWorkspace(targetUri: URI, workspaceRootUri: URI): void { + if (!targetUri.toString().startsWith(workspaceRootUri.toString())) { + throw new Error('Access outside of the workspace is not allowed'); + } + } + /** + * Determines whether a given file or directory should be excluded from workspace operations. + * + * @param stat - The `FileStat` object representing the file or directory to check. + * @returns `true` if the file or directory should be excluded, `false` otherwise. + */ + shouldExclude(stat: FileStat): boolean { + const excludedFolders = ['node_modules', 'lib']; + return stat.resource.path.base.startsWith('.') || excludedFolders.includes(stat.resource.path.base); + } } @injectable() @@ -40,22 +65,21 @@ export class GetWorkspaceDirectoryStructure implements ToolProvider { }; } - @inject(WorkspaceService) - protected workspaceService: WorkspaceService; - @inject(FileService) protected readonly fileService: FileService; - private async getDirectoryStructure(): Promise { - const wsRoots = await this.workspaceService.roots; + @inject(WorkspaceUtils) + protected workspaceUtils: WorkspaceUtils; - if (wsRoots.length === 0) { - throw new Error('Workspace root not found'); + private async getDirectoryStructure(): Promise { + let workspaceRoot; + try { + workspaceRoot = await this.workspaceUtils.getWorkspaceRoot(); + } catch (error) { + return [`Error: ${error.message}`]; } - const workspaceRootUri = wsRoots[0].resource; - - return this.buildDirectoryStructure(workspaceRootUri); + return this.buildDirectoryStructure(workspaceRoot); } private async buildDirectoryStructure(uri: URI, prefix: string = ''): Promise { @@ -64,7 +88,7 @@ export class GetWorkspaceDirectoryStructure implements ToolProvider { if (stat && stat.isDirectory && stat.children) { for (const child of stat.children) { - if (!child.isDirectory || shouldExclude(child)) { continue; }; + if (!child.isDirectory || this.workspaceUtils.shouldExclude(child)) { continue; }; const path = `${prefix}${child.resource.path.base}/`; result.push(path); result.push(...await this.buildDirectoryStructure(child.resource, `${path}`)); @@ -102,31 +126,27 @@ export class FileContentFunction implements ToolProvider { }; } - @inject(WorkspaceService) - protected workspaceService: WorkspaceService; - @inject(FileService) protected readonly fileService: FileService; + @inject(WorkspaceUtils) + protected readonly workspaceUtils: WorkspaceUtils; + private parseArg(arg_string: string): string { const result = JSON.parse(arg_string); return result.file; } private async getFileContent(file: string): Promise { - const wsRoots = await this.workspaceService.roots; - - if (wsRoots.length === 0) { - throw new Error('Workspace root not found'); + let workspaceRoot; + try { + workspaceRoot = await this.workspaceUtils.getWorkspaceRoot(); + } catch (error) { + return JSON.stringify({ error: error.message }); } - const workspaceRootUri = wsRoots[0].resource; - - const targetUri = workspaceRootUri.resolve(file); - - if (!targetUri.toString().startsWith(workspaceRootUri.toString())) { - throw new Error('Access outside of the workspace is not allowed'); - } + const targetUri = workspaceRoot.resolve(file); + this.workspaceUtils.ensureWithinWorkspace(targetUri, workspaceRoot); try { const fileStat = await this.fileService.resolve(targetUri); @@ -171,32 +191,29 @@ export class GetWorkspaceFileList implements ToolProvider { }; } - @inject(WorkspaceService) - protected workspaceService: WorkspaceService; - @inject(FileService) protected readonly fileService: FileService; - async getProjectFileList(path?: string): Promise { - const wsRoots = await this.workspaceService.roots; + @inject(WorkspaceUtils) + protected workspaceUtils: WorkspaceUtils; - if (wsRoots.length === 0) { - throw new Error('Workspace root not found'); + async getProjectFileList(path?: string): Promise { + let workspaceRoot; + try { + workspaceRoot = await this.workspaceUtils.getWorkspaceRoot(); + } catch (error) { + return [`Error: ${error.message}`]; } - const workspaceRootUri = wsRoots[0].resource; - const targetUri = path ? workspaceRootUri.resolve(path) : workspaceRootUri; - - if (!targetUri.toString().startsWith(workspaceRootUri.toString())) { - throw new Error('Access outside of the workspace is not allowed'); - } + const targetUri = path ? workspaceRoot.resolve(path) : workspaceRoot; + this.workspaceUtils.ensureWithinWorkspace(targetUri, workspaceRoot); try { const stat = await this.fileService.resolve(targetUri); if (!stat || !stat.isDirectory) { return ['Error: Directory not found']; } - return await this.listFilesDirectly(targetUri, workspaceRootUri); + return await this.listFilesDirectly(targetUri, workspaceRoot); } catch (error) { return ['Error: Directory not found']; @@ -208,7 +225,7 @@ export class GetWorkspaceFileList implements ToolProvider { const result: string[] = []; if (stat && stat.isDirectory) { - if (shouldExclude(stat)) { + if (this.workspaceUtils.shouldExclude(stat)) { return result; } const children = await this.fileService.resolve(uri); diff --git a/packages/ai-workspace-agent/src/common/template.ts b/packages/ai-workspace-agent/src/common/template.ts index 51897af1eca93..eb03aeea7eb90 100644 --- a/packages/ai-workspace-agent/src/common/template.ts +++ b/packages/ai-workspace-agent/src/common/template.ts @@ -30,8 +30,9 @@ Use the following functions to interact with the workspace files as needed: ### Workspace Navigation Guidelines -1. **Confirm Paths**: Always verify paths by listing directories or files as you navigate. Avoid assumptions based on user input alone. -2. **Start from Root**: Begin at the root and navigate subdirectories step-by-step. +1. **Start at the Root**: For general questions (e.g., "How to build the project"), check root-level documentation files or setup files before browsing subdirectories. +2. **Confirm Paths**: Always verify paths by listing directories or files as you navigate. Avoid assumptions based on user input alone. +3. **Navigate Step-by-Step**: Move into subdirectories only as needed, confirming each directory level. ### Response Guidelines From a7e37fbf4792353850b8dfd050f36bbd15031939 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Tue, 12 Nov 2024 10:09:11 +0100 Subject: [PATCH 4/4] Rename WorkspaceUtils to WorkspaceFunctionScope Signed-off-by: Jonas Helming --- .../src/browser/frontend-module.ts | 4 +-- .../src/browser/functions.ts | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/ai-workspace-agent/src/browser/frontend-module.ts b/packages/ai-workspace-agent/src/browser/frontend-module.ts index ef3f02174dc7a..f0136ce37cc7d 100644 --- a/packages/ai-workspace-agent/src/browser/frontend-module.ts +++ b/packages/ai-workspace-agent/src/browser/frontend-module.ts @@ -17,7 +17,7 @@ import { ContainerModule } from '@theia/core/shared/inversify'; import { ChatAgent } from '@theia/ai-chat/lib/common'; import { Agent, ToolProvider } from '@theia/ai-core/lib/common'; import { WorkspaceAgent } from './workspace-agent'; -import { FileContentFunction, GetWorkspaceDirectoryStructure, GetWorkspaceFileList, WorkspaceUtils } from './functions'; +import { FileContentFunction, GetWorkspaceDirectoryStructure, GetWorkspaceFileList, WorkspaceFunctionScope } from './functions'; export default new ContainerModule(bind => { bind(WorkspaceAgent).toSelf().inSingletonScope(); @@ -26,5 +26,5 @@ export default new ContainerModule(bind => { bind(ToolProvider).to(GetWorkspaceFileList); bind(ToolProvider).to(FileContentFunction); bind(ToolProvider).to(GetWorkspaceDirectoryStructure); - bind(WorkspaceUtils).toSelf().inSingletonScope(); + bind(WorkspaceFunctionScope).toSelf().inSingletonScope(); }); diff --git a/packages/ai-workspace-agent/src/browser/functions.ts b/packages/ai-workspace-agent/src/browser/functions.ts index ee80cca9f675d..ecb35e01c2265 100644 --- a/packages/ai-workspace-agent/src/browser/functions.ts +++ b/packages/ai-workspace-agent/src/browser/functions.ts @@ -22,7 +22,7 @@ import { WorkspaceService } from '@theia/workspace/lib/browser'; import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID } from '../common/functions'; @injectable() -export class WorkspaceUtils { +export class WorkspaceFunctionScope { @inject(WorkspaceService) protected workspaceService: WorkspaceService; @@ -68,13 +68,13 @@ export class GetWorkspaceDirectoryStructure implements ToolProvider { @inject(FileService) protected readonly fileService: FileService; - @inject(WorkspaceUtils) - protected workspaceUtils: WorkspaceUtils; + @inject(WorkspaceFunctionScope) + protected workspaceScope: WorkspaceFunctionScope; private async getDirectoryStructure(): Promise { let workspaceRoot; try { - workspaceRoot = await this.workspaceUtils.getWorkspaceRoot(); + workspaceRoot = await this.workspaceScope.getWorkspaceRoot(); } catch (error) { return [`Error: ${error.message}`]; } @@ -88,7 +88,7 @@ export class GetWorkspaceDirectoryStructure implements ToolProvider { if (stat && stat.isDirectory && stat.children) { for (const child of stat.children) { - if (!child.isDirectory || this.workspaceUtils.shouldExclude(child)) { continue; }; + if (!child.isDirectory || this.workspaceScope.shouldExclude(child)) { continue; }; const path = `${prefix}${child.resource.path.base}/`; result.push(path); result.push(...await this.buildDirectoryStructure(child.resource, `${path}`)); @@ -129,8 +129,8 @@ export class FileContentFunction implements ToolProvider { @inject(FileService) protected readonly fileService: FileService; - @inject(WorkspaceUtils) - protected readonly workspaceUtils: WorkspaceUtils; + @inject(WorkspaceFunctionScope) + protected readonly workspaceScope: WorkspaceFunctionScope; private parseArg(arg_string: string): string { const result = JSON.parse(arg_string); @@ -140,13 +140,13 @@ export class FileContentFunction implements ToolProvider { private async getFileContent(file: string): Promise { let workspaceRoot; try { - workspaceRoot = await this.workspaceUtils.getWorkspaceRoot(); + workspaceRoot = await this.workspaceScope.getWorkspaceRoot(); } catch (error) { return JSON.stringify({ error: error.message }); } const targetUri = workspaceRoot.resolve(file); - this.workspaceUtils.ensureWithinWorkspace(targetUri, workspaceRoot); + this.workspaceScope.ensureWithinWorkspace(targetUri, workspaceRoot); try { const fileStat = await this.fileService.resolve(targetUri); @@ -194,19 +194,19 @@ export class GetWorkspaceFileList implements ToolProvider { @inject(FileService) protected readonly fileService: FileService; - @inject(WorkspaceUtils) - protected workspaceUtils: WorkspaceUtils; + @inject(WorkspaceFunctionScope) + protected workspaceScope: WorkspaceFunctionScope; async getProjectFileList(path?: string): Promise { let workspaceRoot; try { - workspaceRoot = await this.workspaceUtils.getWorkspaceRoot(); + workspaceRoot = await this.workspaceScope.getWorkspaceRoot(); } catch (error) { return [`Error: ${error.message}`]; } const targetUri = path ? workspaceRoot.resolve(path) : workspaceRoot; - this.workspaceUtils.ensureWithinWorkspace(targetUri, workspaceRoot); + this.workspaceScope.ensureWithinWorkspace(targetUri, workspaceRoot); try { const stat = await this.fileService.resolve(targetUri); @@ -225,7 +225,7 @@ export class GetWorkspaceFileList implements ToolProvider { const result: string[] = []; if (stat && stat.isDirectory) { - if (this.workspaceUtils.shouldExclude(stat)) { + if (this.workspaceScope.shouldExclude(stat)) { return result; } const children = await this.fileService.resolve(uri);