Skip to content

Commit

Permalink
Improve Workspace agent functions and prompt (#14426)
Browse files Browse the repository at this point in the history
* Improve Workspace agent functions and prompt

fixed #14361

Signed-off-by: Jonas Helming <[email protected]>
  • Loading branch information
JonasHelming authored Nov 12, 2024
1 parent 8664fac commit 1900a80
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 82 deletions.
4 changes: 3 additions & 1 deletion packages/ai-workspace-agent/src/browser/frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ 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, WorkspaceFunctionScope } from './functions';

export default new ContainerModule(bind => {
bind(WorkspaceAgent).toSelf().inSingletonScope();
bind(Agent).toService(WorkspaceAgent);
bind(ChatAgent).toService(WorkspaceAgent);
bind(ToolProvider).to(GetWorkspaceFileList);
bind(ToolProvider).to(FileContentFunction);
bind(ToolProvider).to(GetWorkspaceDirectoryStructure);
bind(WorkspaceFunctionScope).toSelf().inSingletonScope();
});
202 changes: 156 additions & 46 deletions packages/ai-workspace-agent/src/browser/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,86 @@ 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';

@injectable()
export class WorkspaceFunctionScope {
@inject(WorkspaceService)
protected workspaceService: WorkspaceService;

async getWorkspaceRoot(): Promise<URI> {
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()
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(FileService)
protected readonly fileService: FileService;

@inject(WorkspaceFunctionScope)
protected workspaceScope: WorkspaceFunctionScope;

private async getDirectoryStructure(): Promise<string[]> {
let workspaceRoot;
try {
workspaceRoot = await this.workspaceScope.getWorkspaceRoot();
} catch (error) {
return [`Error: ${error.message}`];
}

return this.buildDirectoryStructure(workspaceRoot);
}

private async buildDirectoryStructure(uri: URI, prefix: string = ''): Promise<string[]> {
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 || this.workspaceScope.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;
Expand All @@ -32,13 +107,15 @@ 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.`,
}
}
},
Expand All @@ -49,27 +126,44 @@ export class FileContentFunction implements ToolProvider {
};
}

@inject(WorkspaceService)
protected workspaceService: WorkspaceService;

@inject(FileService)
protected readonly fileService: FileService;

@inject(WorkspaceFunctionScope)
protected readonly workspaceScope: WorkspaceFunctionScope;

private parseArg(arg_string: string): string {
const result = JSON.parse(arg_string);
return result.file;
}

private async getFileContent(file: string): Promise<string> {
const uri = new URI(file);
const fileContent = await this.fileService.read(uri);
return fileContent.value;
let workspaceRoot;
try {
workspaceRoot = await this.workspaceScope.getWorkspaceRoot();
} catch (error) {
return JSON.stringify({ error: error.message });
}

const targetUri = workspaceRoot.resolve(file);
this.workspaceScope.ensureWithinWorkspace(targetUri, workspaceRoot);

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;
Expand All @@ -78,57 +172,73 @@ 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);
}
};
}

@inject(WorkspaceService)
protected workspaceService: WorkspaceService;

@inject(FileService)
protected readonly fileService: FileService;

async getProjectFileList(): Promise<string[]> {
// Get all files from the workspace service as a flat list of qualified file names
const wsRoots = await this.workspaceService.roots;
const result: string[] = [];
for (const root of wsRoots) {
result.push(...await this.listFilesRecursively(root.resource));
@inject(WorkspaceFunctionScope)
protected workspaceScope: WorkspaceFunctionScope;

async getProjectFileList(path?: string): Promise<string[]> {
let workspaceRoot;
try {
workspaceRoot = await this.workspaceScope.getWorkspaceRoot();
} catch (error) {
return [`Error: ${error.message}`];
}

const targetUri = path ? workspaceRoot.resolve(path) : workspaceRoot;
this.workspaceScope.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, workspaceRoot);

} catch (error) {
return ['Error: Directory not found'];
}
return result;
}

private async listFilesRecursively(uri: URI): Promise<string[]> {
private async listFilesDirectly(uri: URI, workspaceRootUri: URI): Promise<string[]> {
const stat = await this.fileService.resolve(uri);
const result: string[] = [];

if (stat && stat.isDirectory) {
if (this.exclude(stat)) {
if (this.workspaceScope.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;
}
}
1 change: 1 addition & 0 deletions packages/ai-workspace-agent/src/common/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
52 changes: 17 additions & 35 deletions packages/ai-workspace-agent/src/common/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,32 @@
// 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 = <PromptTemplate>{
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. **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.
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.
`
};

0 comments on commit 1900a80

Please sign in to comment.