Skip to content

Commit

Permalink
Streamline the agent code
Browse files Browse the repository at this point in the history
Fix #14818
  • Loading branch information
eneufeld committed Feb 6, 2025
1 parent 35e7cd2 commit 7270813
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 226 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)

## 1.59.0

<a name="breaking_changes_1.59.0">[Breaking Changes:](#breaking_changes_1.59.0)</a>

- [ai] removed constructor from AbstractChatAgent [#14859](https://github.com/eclipse-theia/theia/pull/14859)

## 1.58.0 - 01/30/2025

- [ai] added 'required' property to tool call parameters [#14673](https://github.com/eclipse-theia/theia/pull/14673)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
ChatRequestModelImpl,
lastProgressMessage,
QuestionResponseContentImpl,
SystemMessageDescription,
unansweredQuestions
} from '@theia/ai-chat';
import { Agent, PromptTemplate } from '@theia/ai-core';
Expand Down Expand Up @@ -109,14 +108,19 @@ Do not ask further questions once the text contains 5 or more "Question/Answer"
* This is a very simple example agent that asks questions and continues the conversation based on the user's answers.
*/
@injectable()
export class AskAndContinueChatAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
override id = 'AskAndContinue';
readonly name = 'AskAndContinue';
override defaultLanguageModelPurpose = 'chat';
readonly description = 'This chat will ask questions related to the input and continues after that.';
readonly variables = [];
readonly agentSpecificVariables = [];
readonly functions = [];
export class AskAndContinueChatAgent extends AbstractStreamParsingChatAgent {
id = 'AskAndContinue';
name = 'AskAndContinue';
override description = 'This chat will ask questions related to the input and continues after that.';
protected defaultLanguageModelPurpose = 'chat';
override languageModelRequirements = [
{
purpose: 'chat',
identifier: 'openai/gpt-4o',
}
];
override promptTemplates = [systemPrompt];
protected override systemPromptId: string | undefined = systemPrompt.id;

@postConstruct()
addContentMatchers(): void {
Expand All @@ -133,20 +137,6 @@ export class AskAndContinueChatAgent extends AbstractStreamParsingChatAgent impl
});
}

override languageModelRequirements = [
{
purpose: 'chat',
identifier: 'openai/gpt-4o',
}
];

readonly promptTemplates = [systemPrompt];

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

protected override async onResponseComplete(request: ChatRequestModelImpl): Promise<void> {
const unansweredQs = unansweredQuestions(request);
if (unansweredQs.length < 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
SystemMessageDescription
} from '@theia/ai-chat';
import { ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
import { Agent, PromptTemplate } from '@theia/ai-core';
import { Agent, LanguageModelRequirement } from '@theia/ai-core';
import { URI } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
Expand All @@ -39,16 +39,12 @@ export function bindChangeSetChatAgentContribution(bind: interfaces.Bind): void
* This is a test agent demonstrating how to create change sets in AI chats.
*/
@injectable()
export class ChangeSetChatAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
override id = 'ChangeSet';
export class ChangeSetChatAgent extends AbstractStreamParsingChatAgent {
readonly id = 'ChangeSet';
readonly name = 'ChangeSet';
override defaultLanguageModelPurpose = 'chat';
readonly description = 'This chat will create and modify a change set.';
readonly variables = [];
readonly agentSpecificVariables = [];
readonly functions = [];
override languageModelRequirements = [];
promptTemplates: PromptTemplate[] = [];
readonly defaultLanguageModelPurpose = 'chat';
override readonly description = 'This chat will create and modify a change set.';
override languageModelRequirements: LanguageModelRequirement[] = [];

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
Expand Down
41 changes: 27 additions & 14 deletions packages/ai-chat/src/common/chat-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatAgents.ts

import {
AgentSpecificVariables,
CommunicationRecordingService,
getTextOfResponse,
LanguageModel,
LanguageModelRequirement,
LanguageModelResponse,
LanguageModelStreamResponse,
PromptService,
PromptTemplate,
ResolvedPromptTemplate,
ToolRequest,
} from '@theia/ai-core';
Expand All @@ -39,7 +41,7 @@ import {
MessageActor,
} from '@theia/ai-core/lib/common';
import { CancellationToken, ContributionProvider, ILogger, isArray } from '@theia/core';
import { inject, injectable, named, postConstruct, unmanaged } from '@theia/core/shared/inversify';
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
import { ChatAgentService } from './chat-agent-service';
import {
ChatModel,
Expand Down Expand Up @@ -119,7 +121,7 @@ export interface ChatAgent extends Agent {
}

@injectable()
export abstract class AbstractChatAgent {
export abstract class AbstractChatAgent implements ChatAgent {
@inject(LanguageModelRegistry) protected languageModelRegistry: LanguageModelRegistry;
@inject(ILogger) protected logger: ILogger;
@inject(CommunicationRecordingService) protected recordingService: CommunicationRecordingService;
Expand All @@ -128,21 +130,26 @@ export abstract class AbstractChatAgent {

@inject(ContributionProvider) @named(ResponseContentMatcherProvider)
protected contentMatcherProviders: ContributionProvider<ResponseContentMatcherProvider>;
protected additionalToolRequests: ToolRequest[] = [];
protected contentMatchers: ResponseContentMatcher[] = [];

@inject(DefaultResponseContentFactory)
protected defaultContentFactory: DefaultResponseContentFactory;

constructor(
@unmanaged() public id: string,
@unmanaged() public languageModelRequirements: LanguageModelRequirement[],
@unmanaged() protected defaultLanguageModelPurpose: string,
@unmanaged() public iconClass: string = 'codicon codicon-copilot',
@unmanaged() public locations: ChatAgentLocation[] = ChatAgentLocation.ALL,
@unmanaged() public tags: string[] = ['Chat'],
@unmanaged() public defaultLogging: boolean = true) {
}
readonly abstract id: string;
readonly abstract name: string;
readonly abstract languageModelRequirements: LanguageModelRequirement[];
iconClass: string = 'codicon codicon-copilot';
locations: ChatAgentLocation[] = ChatAgentLocation.ALL;
tags: string[] = ['Chat'];
description: string = '';
variables: string[] = [];
promptTemplates: PromptTemplate[] = [];
agentSpecificVariables: AgentSpecificVariables[] = [];
functions: string[] = [];
protected readonly abstract defaultLanguageModelPurpose: string;
protected defaultLogging: boolean = true;
protected systemPromptId: string | undefined = undefined;
protected additionalToolRequests: ToolRequest[] = [];
protected contentMatchers: ResponseContentMatcher[] = [];

@postConstruct()
init(): void {
Expand Down Expand Up @@ -236,7 +243,13 @@ export abstract class AbstractChatAgent {
return languageModel;
}

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

protected async getMessages(
model: ChatModel, includeResponseInProgress = false
Expand Down
53 changes: 21 additions & 32 deletions packages/ai-chat/src/common/command-chat-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@
// *****************************************************************************

import { inject, injectable } from '@theia/core/shared/inversify';
import { AbstractTextToModelParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents';
import {
PromptTemplate,
AgentSpecificVariables
} from '@theia/ai-core';
import { AbstractTextToModelParsingChatAgent, SystemMessageDescription } from './chat-agents';
import { LanguageModelRequirement, PromptTemplate } from '@theia/ai-core';
import {
ChatRequestModelImpl,
ChatResponseContent,
Expand Down Expand Up @@ -250,38 +247,30 @@ interface ParsedCommand {
}

@injectable()
export class CommandChatAgent extends AbstractTextToModelParsingChatAgent<ParsedCommand> implements ChatAgent {
export class CommandChatAgent extends AbstractTextToModelParsingChatAgent<ParsedCommand> {
@inject(CommandRegistry)
protected commandRegistry: CommandRegistry;
@inject(MessageService)
protected messageService: MessageService;
readonly name: string;
readonly description: string;
readonly variables: string[];
readonly promptTemplates: PromptTemplate[];
readonly functions: string[];
readonly agentSpecificVariables: AgentSpecificVariables[];

constructor(
) {
super('Command', [{
purpose: 'command',
identifier: 'openai/gpt-4o',
}], 'command');
this.name = 'Command';
this.description = 'This agent is aware of all commands that the user can execute within the Theia IDE, the tool that the user is currently working with. \
Based on the user request, it can find the right command and then let the user execute it.';
this.variables = [];
this.promptTemplates = [commandTemplate];
this.functions = [];
this.agentSpecificVariables = [{
name: 'command-ids',
description: 'The list of available commands in Theia.',
usedInPrompt: true
}];
}

protected async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
id: string = 'Command';
name = 'Command';
languageModelRequirements: LanguageModelRequirement[] = [{
purpose: 'command',
identifier: 'openai/gpt-4o',
}];
protected defaultLanguageModelPurpose: string = 'command';

override description = 'This agent is aware of all commands that the user can execute within the Theia IDE, the tool that the user is currently working with. \
Based on the user request, it can find the right command and then let the user execute it.';
override promptTemplates = [commandTemplate];
override agentSpecificVariables = [{
name: 'command-ids',
description: 'The list of available commands in Theia.',
usedInPrompt: true
}];

protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
const knownCommands: string[] = [];
for (const command of this.commandRegistry.getAllCommands()) {
knownCommands.push(`${command.id}: ${command.label}`);
Expand Down
28 changes: 8 additions & 20 deletions packages/ai-chat/src/common/custom-chat-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,17 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { AgentSpecificVariables, PromptTemplate } from '@theia/ai-core';
import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents';
import { LanguageModelRequirement } from '@theia/ai-core';
import { AbstractStreamParsingChatAgent } from './chat-agents';
import { injectable } from '@theia/core/shared/inversify';

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

constructor(
) {
super('CustomChatAgent', [{ purpose: 'chat' }], 'chat');
}
protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
const resolvedPrompt = await this.promptService.getPrompt(`${this.name}_prompt`);
return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined;
}
export class CustomChatAgent extends AbstractStreamParsingChatAgent {
id: string = 'CustomChatAgent';
name: string = 'CustomChatAgent';
languageModelRequirements: LanguageModelRequirement[] = [{ purpose: 'chat' }];
protected defaultLanguageModelPurpose: string = 'chat';
protected override systemPromptId: string = `${this.name}_prompt`;

set prompt(prompt: string) {
this.promptTemplates.push({ id: `${this.name}_prompt`, template: prompt });
Expand Down
55 changes: 22 additions & 33 deletions packages/ai-chat/src/common/orchestrator-chat-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { AgentSpecificVariables, getJsonOfText, getTextOfResponse, LanguageModelResponse } from '@theia/ai-core';
import {
PromptTemplate
} from '@theia/ai-core/lib/common';
import { getJsonOfText, getTextOfResponse, LanguageModelRequirement, LanguageModelResponse } from '@theia/ai-core';
import { PromptTemplate } from '@theia/ai-core/lib/common';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ChatAgentService } from './chat-agent-service';
import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents';
import { AbstractStreamParsingChatAgent } from './chat-agents';
import { ChatRequestModelImpl, InformationalChatResponseContentImpl } from './chat-model';
import { generateUuid } from '@theia/core';
import { ChatHistoryEntry } from './chat-history-entry';
Expand Down Expand Up @@ -66,29 +64,25 @@ export const OrchestratorChatAgentId = 'Orchestrator';
const OrchestratorRequestIdKey = 'orchestatorRequestIdKey';

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

constructor() {
super(OrchestratorChatAgentId, [{
purpose: 'agent-selection',
identifier: 'openai/gpt-4o',
}], 'agent-selection', 'codicon codicon-symbol-boolean', undefined, undefined, false);
this.name = OrchestratorChatAgentId;
this.description = 'This agent analyzes the user request against the description of all available chat agents and selects the best fitting agent to answer the request \
(by using AI).The user\'s request will be directly delegated to the selected agent without further confirmation.';
this.variables = ['chatAgents'];
this.promptTemplates = [orchestratorTemplate];
this.fallBackChatAgentId = 'Universal';
this.functions = [];
this.agentSpecificVariables = [];
}
export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent {
id: string = OrchestratorChatAgentId;
name = OrchestratorChatAgentId;
languageModelRequirements: LanguageModelRequirement[] = [{
purpose: 'agent-selection',
identifier: 'openai/gpt-4o',
}];
protected defaultLanguageModelPurpose: string = 'agent-selection';

override variables = ['chatAgents'];
override promptTemplates = [orchestratorTemplate];
override description = 'This agent analyzes the user request against the description of all available chat agents and selects the best fitting agent to answer the request \
(by using AI).The user\'s request will be directly delegated to the selected agent without further confirmation.';
override iconClass: string = 'codicon codicon-symbol-boolean';

protected override defaultLogging = false;
protected override systemPromptId: string = orchestratorTemplate.id;

private fallBackChatAgentId = 'Universal';

@inject(ChatAgentService)
protected chatAgentService: ChatAgentService;
Expand All @@ -110,11 +104,6 @@ export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent implem
return super.invoke(request);
}

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

protected override async addContentsToResponse(response: LanguageModelResponse, request: ChatRequestModelImpl): Promise<void> {
let agentIds: string[] = [];
const responseText = await getTextOfResponse(response);
Expand Down
Loading

0 comments on commit 7270813

Please sign in to comment.