Skip to content

Commit

Permalink
Add support for change sets via tool functions
Browse files Browse the repository at this point in the history
fixed #14678

Signed-off-by: Jonas Helming <[email protected]>
  • Loading branch information
JonasHelming committed Jan 12, 2025
1 parent e6ca7ad commit e20ddb8
Show file tree
Hide file tree
Showing 14 changed files with 837 additions and 13 deletions.
49 changes: 49 additions & 0 deletions packages/ai-workspace-agent/src/browser/coder-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from '@theia/ai-chat/lib/common';
import { AgentSpecificVariables, PromptTemplate } from '@theia/ai-core';
import { injectable } from '@theia/core/shared/inversify';
import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID, GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID } from '../common/workspace-functions';
import { coderReplaceTemplate } from '../common/coder-replace-template';

@injectable()
export class CoderAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
name: string;
description: string;
promptTemplates: PromptTemplate[];
variables: never[];
readonly agentSpecificVariables: AgentSpecificVariables[];
readonly functions: string[];

constructor() {
super('Coder', [{
purpose: 'chat',
identifier: 'openai/gpt-4o',
}], 'chat');
this.name = 'Coder';
this.description = 'An AI assistant integrated into Theia IDE, designed to assist software developers with code tasks.';
this.promptTemplates = [coderReplaceTemplate];
this.variables = [];
this.agentSpecificVariables = [];
this.functions = [GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FILE_CONTENT_FUNCTION_ID];
}

protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
const resolvedPrompt = await this.promptService.getPrompt(coderReplaceTemplate.id);
return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the
// Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
// version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { ContributionProvider } from '@theia/core';
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';

/**
* A change operation to be applied to content by a {@link ContentChangeApplier}.
*/
export interface ChangeOperation {
kind: string;
}

/**
* A provider capable of applying a content change operation.
*
* Implementations of this interface are responsible for modifying
* a given content string based on the corresponding {@link ChangeOperation}.
*
* @template T The type of change operation.
*/
export interface ContentChangeApplier<T extends ChangeOperation> {
/**
* The kind of change operation that this applier can handle.
*/
kind: T['kind'];
/**
* Applies a change operation to the provided content.
*
* @param content The original content.
* @param operation The change operation to apply.
* @returns The updated content.
*/
applyChange(content: string, operation: T): string;
}

export const ContentChangeApplier = Symbol('ContentChangeApplier');

/**
* A service that applies content changes using registered ContentChangeApplier contributions.
*
* This class collects multiple implementations of the {@link ContentChangeApplier}
* interface and applies a series of change operations to a given content string.
*/
@injectable()
export class ContentChangeApplierService {

@inject(ContributionProvider)
@named(ContentChangeApplier)
public providers: ContributionProvider<ContentChangeApplier<ChangeOperation>>;

private readonly applierMap = new Map<string, ContentChangeApplier<ChangeOperation>>();

@postConstruct()
init(): void {
this.providers.getContributions().forEach(provider => {
this.registerContentChangeApplier(provider.kind, provider);
});
}
/**
* Registers a {@link ContentChangeApplier} for a specific operation kind.
*
* @param kind The change operation kind (e.g. 'replace', 'diffBased')
* @param provider The ContentChangeApplier implementation that handles operations of that kind.
*/
registerContentChangeApplier(kind: string, provider: ContentChangeApplier<ChangeOperation>): void {
this.applierMap.set(kind, provider);
}

/**
* Applies an array of changes to the given content.
*
* Each change operation in the array is applied sequentially to update the content.
*
* @param content The initial content.
* @param changes An array of change operations.
* @returns The modified content after all operations have been applied.
* @throws Error If a change operation's kind does not have a registered ContentChangeApplier.
*/
public applyChangesToContent(content: string, changes: ChangeOperation[]): string {
for (const operation of changes) {
const applier = this.applierMap.get(operation.kind);
if (!applier) {
throw new Error(`No ContentChangeApplier found for operation kind: ${operation.kind}`);
}
content = applier.applyChange(content, operation);
}
return content;
}
}
213 changes: 213 additions & 0 deletions packages/ai-workspace-agent/src/browser/file-changeset-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { ToolProvider, ToolRequest } from '@theia/ai-core';
import { FileChangeSetService } from './file-changeset-service';

@injectable()
export class InitializeChangeSetProvider implements ToolProvider {
static ID = 'changeSet_initializeChangeSet';

@inject(FileChangeSetService)
protected readonly changeSetService: FileChangeSetService;

getTool(): ToolRequest {
return {
id: InitializeChangeSetProvider.ID,
name: InitializeChangeSetProvider.ID,
description: 'Creates a new change set with a unique UUID and description.',
parameters: {
type: 'object',
properties: {
uuid: { type: 'string', description: 'Unique identifier for the change set.' },
description: { type: 'string', description: 'High-level description of the change set.' }
},
required: ['uuid', 'description']
},
handler: async (args: string): Promise<string> => {
try {
const { uuid, description } = JSON.parse(args);
this.changeSetService.initializeChangeSet(uuid, description);
return `Change set ${uuid} initialized successfully.`;
} catch (error) {
return JSON.stringify({ error: error.message });
}
}
};
}
}

@injectable()
export class RemoveFileChangeProvider implements ToolProvider {
static ID = 'changeSet_removeFileChange';

@inject(FileChangeSetService)
protected readonly changeSetService: FileChangeSetService;

getTool(): ToolRequest {
return {
id: RemoveFileChangeProvider.ID,
name: RemoveFileChangeProvider.ID,
description: 'Removes a file and all related changes from the specified change set.',
parameters: {
type: 'object',
properties: {
uuid: { type: 'string', description: 'Unique identifier for the change set.' },
filePath: { type: 'string', description: 'Path to the file.' }
},
required: ['uuid', 'filePath']
},
handler: async (args: string): Promise<string> => {
try {
const { uuid, filePath } = JSON.parse(args);
this.changeSetService.removeFileChange(uuid, filePath);
return `File ${filePath} removed from change set ${uuid}.`;
} catch (error) {
return JSON.stringify({ error: error.message });
}
}
};
}
}

@injectable()
export class ListChangedFilesProvider implements ToolProvider {
static ID = 'changeSet_listChangedFiles';

@inject(FileChangeSetService)
protected readonly changeSetService: FileChangeSetService;

getTool(): ToolRequest {
return {
id: ListChangedFilesProvider.ID,
name: ListChangedFilesProvider.ID,
description: 'Lists all files included in a specific change set.',
parameters: {
type: 'object',
properties: {
uuid: { type: 'string', description: 'Unique identifier for the change set.' }
},
required: ['uuid']
},
handler: async (args: string): Promise<string> => {
try {
const { uuid } = JSON.parse(args);
const files = this.changeSetService.listChangedFiles(uuid);
return JSON.stringify(files);
} catch (error) {
return JSON.stringify({ error: error.message });
}
}
};
}
}

@injectable()
export class GetFileChangesProvider implements ToolProvider {
static ID = 'changeSet_getFileChanges';

@inject(FileChangeSetService)
protected readonly changeSetService: FileChangeSetService;

getTool(): ToolRequest {
return {
id: GetFileChangesProvider.ID,
name: GetFileChangesProvider.ID,
description: 'Fetches the operations of a specific file in a change set.',
parameters: {
type: 'object',
properties: {
uuid: { type: 'string', description: 'Unique identifier for the change set.' },
filePath: { type: 'string', description: 'Path to the file.' }
},
required: ['uuid', 'filePath']
},
handler: async (args: string): Promise<string> => {
try {
const { uuid, filePath } = JSON.parse(args);
const changes = this.changeSetService.getFileChanges(uuid, filePath);
return JSON.stringify(changes);
} catch (error) {
return JSON.stringify({ error: error.message });
}
}
};
}
}

@injectable()
export class GetChangeSetProvider implements ToolProvider {
static ID = 'changeSet_getChangeSet';

@inject(FileChangeSetService)
protected readonly changeSetService: FileChangeSetService;

getTool(): ToolRequest {
return {
id: GetChangeSetProvider.ID,
name: GetChangeSetProvider.ID,
description: 'Fetches the details of a specific change set.',
parameters: {
type: 'object',
properties: {
uuid: { type: 'string', description: 'Unique identifier for the change set.' }
},
required: ['uuid']
},
handler: async (args: string): Promise<string> => {
try {
const { uuid } = JSON.parse(args);
const changeSet = this.changeSetService.getChangeSet(uuid);
return JSON.stringify(changeSet);
} catch (error) {
return JSON.stringify({ error: error.message });
}
}
};
}
}

@injectable()
export class ApplyChangeSetProvider implements ToolProvider {
static ID = 'changeSet_applyChangeSet';

@inject(FileChangeSetService)
protected readonly changeSetService: FileChangeSetService;

getTool(): ToolRequest {
return {
id: ApplyChangeSetProvider.ID,
name: ApplyChangeSetProvider.ID,
description: 'Applies the specified change set by UUID, executing all file modifications described within.',
parameters: {
type: 'object',
properties: {
uuid: { type: 'string', description: 'Unique identifier for the change set to apply.' }
},
required: ['uuid']
},
handler: async (args: string): Promise<string> => {
try {
const { uuid } = JSON.parse(args);
await this.changeSetService.applyChangeSet(uuid);
return `Change set ${uuid} applied successfully.`;
} catch (error) {
return JSON.stringify({ error: error.message });
}
}
};
}
}
Loading

0 comments on commit e20ddb8

Please sign in to comment.