From 75284382abf73b187d0669acb293497c742e7db8 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Fri, 11 Oct 2024 18:50:34 +0200 Subject: [PATCH 1/5] feat: add support for custom agents This adds support for custom agents which are basically a custom system prompt with additional metadata (id, name and description). This features allows to add very specific agents without coding. All features, like variable and functions are supported. --- .../src/browser/ai-chat-frontend-module.ts | 25 ++++++- .../src/browser/custom-agent-factory.ts | 20 +++++ ...agent-frontend-application-contribution.ts | 73 +++++++++++++++++++ .../ai-chat/src/common/chat-agent-service.ts | 15 ++++ .../ai-chat/src/common/custom-chat-agent.ts | 44 +++++++++++ packages/ai-chat/src/common/index.ts | 7 +- packages/ai-core/package.json | 2 + .../agent-configuration-widget.tsx | 6 ++ .../frontend-prompt-customization-service.ts | 37 +++++++++- packages/ai-core/src/common/agent-service.ts | 29 +++++++- packages/ai-core/src/common/prompt-service.ts | 37 ++++++++++ yarn.lock | 36 ++++++++- 12 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 packages/ai-chat/src/browser/custom-agent-factory.ts create mode 100644 packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts create mode 100644 packages/ai-chat/src/common/custom-chat-agent.ts diff --git a/packages/ai-chat/src/browser/ai-chat-frontend-module.ts b/packages/ai-chat/src/browser/ai-chat-frontend-module.ts index c2231c64734aa..cd84c076f6c5f 100644 --- a/packages/ai-chat/src/browser/ai-chat-frontend-module.ts +++ b/packages/ai-chat/src/browser/ai-chat-frontend-module.ts @@ -14,9 +14,9 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Agent, AIVariableContribution } from '@theia/ai-core/lib/common'; +import { Agent, AgentService, AIVariableContribution } from '@theia/ai-core/lib/common'; import { bindContributionProvider } from '@theia/core'; -import { PreferenceContribution } from '@theia/core/lib/browser'; +import { FrontendApplicationContribution, PreferenceContribution } from '@theia/core/lib/browser'; import { ContainerModule } from '@theia/core/shared/inversify'; import { ChatAgent, @@ -27,13 +27,16 @@ import { ChatService, DefaultChatAgentId } from '../common'; +import { ChatAgentsVariableContribution } from '../common/chat-agents-variable-contribution'; import { CommandChatAgent } from '../common/command-chat-agents'; +import { CustomChatAgent } from '../common/custom-chat-agent'; import { OrchestratorChatAgent, OrchestratorChatAgentId } from '../common/orchestrator-chat-agent'; +import { DefaultResponseContentFactory, DefaultResponseContentMatcherProvider, ResponseContentMatcherProvider } from '../common/response-content-matcher'; import { UniversalChatAgent } from '../common/universal-chat-agent'; import { aiChatPreferences } from './ai-chat-preferences'; -import { ChatAgentsVariableContribution } from '../common/chat-agents-variable-contribution'; +import { AICustomAgentsFrontendApplicationContribution } from './custom-agent-frontend-application-contribution'; import { FrontendChatServiceImpl } from './frontend-chat-service'; -import { DefaultResponseContentMatcherProvider, DefaultResponseContentFactory, ResponseContentMatcherProvider } from '../common/response-content-matcher'; +import { FactoryOfCustomAgents } from './custom-agent-factory'; export default new ContainerModule(bind => { bindContributionProvider(bind, Agent); @@ -69,4 +72,18 @@ export default new ContainerModule(bind => { bind(ChatAgent).toService(CommandChatAgent); bind(PreferenceContribution).toConstantValue({ schema: aiChatPreferences }); + + bind(CustomChatAgent).toSelf(); + bind(FactoryOfCustomAgents).toFactory( + ctx => (id: string, name: string, description: string, prompt: string) => { + const agent = ctx.container.get(CustomChatAgent); + agent.id = id; + agent.name = name; + agent.description = description; + agent.prompt = prompt; + ctx.container.get(ChatAgentService).registerChatAgent(agent); + ctx.container.get(AgentService).registerAgent(agent); + return agent; + }); + bind(FrontendApplicationContribution).to(AICustomAgentsFrontendApplicationContribution).inSingletonScope(); }); diff --git a/packages/ai-chat/src/browser/custom-agent-factory.ts b/packages/ai-chat/src/browser/custom-agent-factory.ts new file mode 100644 index 0000000000000..dbf94cfa49c2e --- /dev/null +++ b/packages/ai-chat/src/browser/custom-agent-factory.ts @@ -0,0 +1,20 @@ +// ***************************************************************************** +// 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 { CustomChatAgent } from '../common'; + +export const FactoryOfCustomAgents = Symbol('FactoryOfCustomAgents'); +export type FactoryOfCustomAgents = (id: string, name: string, description: string, prompt: string) => CustomChatAgent; diff --git a/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts b/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts new file mode 100644 index 0000000000000..e6cea408e2866 --- /dev/null +++ b/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts @@ -0,0 +1,73 @@ +// ***************************************************************************** +// 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 { AgentService, CustomAgentDescription, PromptCustomizationService } from '@theia/ai-core'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ChatAgentService } from '../common'; +import { FactoryOfCustomAgents } from './custom-agent-factory'; + +@injectable() +export class AICustomAgentsFrontendApplicationContribution implements FrontendApplicationContribution { + @inject(FactoryOfCustomAgents) + protected readonly customAgentFactory: FactoryOfCustomAgents; + + @inject(PromptCustomizationService) + protected readonly customizationService: PromptCustomizationService; + + @inject(AgentService) + private readonly agentService: AgentService; + + @inject(ChatAgentService) + private readonly chatAgentService: ChatAgentService; + + private knownCustomAgents: Map = new Map(); + onStart(): void { + this.customizationService?.getCustomAgents().then(customAgents => { + customAgents.forEach(agent => { + this.customAgentFactory(agent.id, agent.name, agent.description, agent.prompt); + this.knownCustomAgents.set(agent.id, agent); + }); + }).catch(e => { + console.error('Failed to load custom agents', e); + }); + this.customizationService?.onDidChangeCustomAgents(() => { + this.customizationService?.getCustomAgents().then(customAgents => { + const customAgentsToAdd = customAgents.filter(agent => + !this.knownCustomAgents.has(agent.id) || !CustomAgentDescription.equals(this.knownCustomAgents.get(agent.id)!, agent)); + const customAgentIdsToRemove = [...this.knownCustomAgents.values()].filter(agent => + !customAgents.find(a => CustomAgentDescription.equals(a, agent))).map(a => a.id); + + // delete first so we don't have to deal with the case where we add and remove the same agentId + customAgentIdsToRemove.forEach(id => { + this.chatAgentService.unregisterChatAgent(id); + this.agentService.unregisterAgent(id); + this.knownCustomAgents.delete(id); + }); + customAgentsToAdd + .forEach(agent => { + this.customAgentFactory(agent.id, agent.name, agent.description, agent.prompt); + this.knownCustomAgents.set(agent.id, agent); + }); + }).catch(e => { + console.error('Failed to load custom agents', e); + }); + }); + } + + onStop(): void { + } +} diff --git a/packages/ai-chat/src/common/chat-agent-service.ts b/packages/ai-chat/src/common/chat-agent-service.ts index 53704d427cfd0..7c01541d44be1 100644 --- a/packages/ai-chat/src/common/chat-agent-service.ts +++ b/packages/ai-chat/src/common/chat-agent-service.ts @@ -41,6 +41,18 @@ export interface ChatAgentService { * Returns all agents, including disabled ones. */ getAllAgents(): ChatAgent[]; + + /** + * Allows to register a chat agent programmatically. + * @param agent the agent to register + */ + registerChatAgent(agent: ChatAgent): void; + + /** + * Allows to unregister a chat agent programmatically. + * @param agentId the agent id to unregister + */ + unregisterChatAgent(agentId: string): void; } @injectable() export class ChatAgentServiceImpl implements ChatAgentService { @@ -65,6 +77,9 @@ export class ChatAgentServiceImpl implements ChatAgentService { registerChatAgent(agent: ChatAgent): void { this._agents.push(agent); } + unregisterChatAgent(agentId: string): void { + this._agents = this._agents.filter(a => a.id !== agentId); + } getAgent(id: string): ChatAgent | undefined { if (!this._agentIsEnabled(id)) { diff --git a/packages/ai-chat/src/common/custom-chat-agent.ts b/packages/ai-chat/src/common/custom-chat-agent.ts new file mode 100644 index 0000000000000..5d0cdc90ceb7d --- /dev/null +++ b/packages/ai-chat/src/common/custom-chat-agent.ts @@ -0,0 +1,44 @@ +// ***************************************************************************** +// 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 { AgentSpecificVariables, PromptTemplate } from '@theia/ai-core'; +import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } 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 { + const resolvedPrompt = await this.promptService.getPrompt(`${this.name}_prompt`); + return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined; + } + + set prompt(prompt: string) { + this.promptTemplates.push({ id: `${this.name}_system`, template: prompt }); + } +} diff --git a/packages/ai-chat/src/common/index.ts b/packages/ai-chat/src/common/index.ts index 7fdd58621cc6b..cf160ddcadf10 100644 --- a/packages/ai-chat/src/common/index.ts +++ b/packages/ai-chat/src/common/index.ts @@ -13,12 +13,13 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -export * from './chat-agent-service'; export * from './chat-agents'; +export * from './chat-agent-service'; export * from './chat-model'; -export * from './parsed-chat-request'; export * from './chat-request-parser'; export * from './chat-service'; export * from './command-chat-agents'; -export * from './universal-chat-agent'; +export * from './custom-chat-agent'; +export * from './parsed-chat-request'; export * from './orchestrator-chat-agent'; +export * from './universal-chat-agent'; diff --git a/packages/ai-core/package.json b/packages/ai-core/package.json index e40cc0906d1a3..88a834aa8ab06 100644 --- a/packages/ai-core/package.json +++ b/packages/ai-core/package.json @@ -11,6 +11,8 @@ "@theia/output": "1.54.0", "@theia/variable-resolver": "1.54.0", "@theia/workspace": "1.54.0", + "@types/js-yaml": "^4.0.9", + "js-yaml": "^4.1.0", "minimatch": "^5.1.0", "tslib": "^2.6.2" }, diff --git a/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx b/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx index 43b23b8c9060a..5b2ad05974237 100644 --- a/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx +++ b/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx @@ -87,6 +87,7 @@ export class AIAgentConfigurationWidget extends ReactWidget { this.aiSettingsService.onDidChange(() => this.update()); this.aiConfigurationSelectionService.onDidAgentChange(() => this.update()); + this.agentService.onDidChangeAgents(() => this.update()); this.update(); } @@ -100,6 +101,7 @@ export class AIAgentConfigurationWidget extends ReactWidget { )} +
{this.renderAgentDetails()} @@ -205,6 +207,10 @@ export class AIAgentConfigurationWidget extends ReactWidget { this.aiConfigurationSelectionService.selectConfigurationTab(AIVariableConfigurationWidget.ID); } + protected addCustomAgent(): void { + this.promptCustomizationService.openCustomAgentYaml(); + } + protected setActiveAgent(agent: Agent): void { this.aiConfigurationSelectionService.setActiveAgent(agent); this.update(); diff --git a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts index aff47785cb315..27d44a342f8a9 100644 --- a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts +++ b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts @@ -17,13 +17,14 @@ import { DisposableCollection, URI, Event, Emitter } from '@theia/core'; import { OpenerService } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; -import { PromptCustomizationService, PromptTemplate } from '../common'; +import { PromptCustomizationService, PromptTemplate, CustomAgentDescription } from '../common'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileChangesEvent } from '@theia/filesystem/lib/common/files'; import { AICorePreferences, PREFERENCE_NAME_PROMPT_TEMPLATES } from './ai-core-preferences'; import { AgentService } from '../common/agent-service'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { load } from 'js-yaml'; @injectable() export class FrontendPromptCustomizationServiceImpl implements PromptCustomizationService { @@ -51,6 +52,9 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati private readonly onDidChangePromptEmitter = new Emitter(); readonly onDidChangePrompt: Event = this.onDidChangePromptEmitter.event; + private readonly onDidChangeCustomAgentsEmitter = new Emitter(); + readonly onDidChangeCustomAgents = this.onDidChangeCustomAgentsEmitter.event; + @postConstruct() protected init(): void { this.preferences.onPreferenceChanged(event => { @@ -72,6 +76,9 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati this.toDispose.push(this.fileService.watch(templateURI, { recursive: true, excludes: [] })); this.toDispose.push(this.fileService.onDidFilesChange(async (event: FileChangesEvent) => { + if (event.changes.some(change => change.resource.toString().endsWith('customAgents.yml'))) { + this.onDidChangeCustomAgentsEmitter.fire(); + } // check deleted templates for (const deletedFile of event.getDeleted()) { if (this.trackedTemplateURIs.has(deletedFile.resource.toString())) { @@ -194,4 +201,32 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati return undefined; } + async getCustomAgents(): Promise { + const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml'); + const yamlExists = await this.fileService.exists(customAgentYamlUri); + if (!yamlExists) { + return []; + } + const filecontent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' }); + try { + const doc = load(filecontent.value); + if (!Array.isArray(doc) || !doc.every(entry => CustomAgentDescription.is(entry))) { + console.debug('Invalid customAgents.yml file content'); + return []; + } + return doc as CustomAgentDescription[]; + } catch (e) { + console.debug(e.message, e); + return []; + } + } + + async openCustomAgentYaml(): Promise { + const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml'); + if (! await this.fileService.exists(customAgentYamlUri)) { + await this.fileService.createFile(customAgentYamlUri); + } + const openHandler = await this.openerService.getOpener(customAgentYamlUri); + openHandler.open(customAgentYamlUri); + } } diff --git a/packages/ai-core/src/common/agent-service.ts b/packages/ai-core/src/common/agent-service.ts index 1038864bf81e3..7bb5b0f01a57a 100644 --- a/packages/ai-core/src/common/agent-service.ts +++ b/packages/ai-core/src/common/agent-service.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { inject, injectable, named, optional, postConstruct } from '@theia/core/shared/inversify'; -import { ContributionProvider } from '@theia/core'; +import { ContributionProvider, Emitter, Event } from '@theia/core'; import { Agent } from './agent'; import { AISettingsService } from './settings-service'; @@ -48,6 +48,24 @@ export interface AgentService { * @return true if the agent is enabled, false otherwise. */ isEnabled(agentId: string): boolean; + + /** + * Allows to register an agent programmatically. + * @param agent the agent to register + */ + registerAgent(agent: Agent): void; + + /** + * Allows to unregister an agent programmatically. + * @param agentId the agent id to unregister + */ + unregisterAgent(agentId: string): void; + + /** + * Emitted when the list of agents changes. + * This can be used to update the UI when agents are added or removed. + */ + onDidChangeAgents: Event; } @injectable() @@ -63,6 +81,9 @@ export class AgentServiceImpl implements AgentService { protected _agents: Agent[] = []; + private readonly onDidChangeAgentsEmitter = new Emitter(); + readonly onDidChangeAgents = this.onDidChangeAgentsEmitter.event; + @postConstruct() protected init(): void { this.aiSettingsService?.getSettings().then(settings => { @@ -82,6 +103,12 @@ export class AgentServiceImpl implements AgentService { registerAgent(agent: Agent): void { this._agents.push(agent); + this.onDidChangeAgentsEmitter.fire(); + } + + unregisterAgent(agentId: string): void { + this._agents = this._agents.filter(a => a.id !== agentId); + this.onDidChangeAgentsEmitter.fire(); } getAgents(): Agent[] { diff --git a/packages/ai-core/src/common/prompt-service.ts b/packages/ai-core/src/common/prompt-service.ts index 8bc55bedebd8b..de6f4446b94ca 100644 --- a/packages/ai-core/src/common/prompt-service.ts +++ b/packages/ai-core/src/common/prompt-service.ts @@ -69,6 +69,27 @@ export interface PromptService { getAllPrompts(): PromptMap; } +export interface CustomAgentDescription { + id: string; + name: string; + description: string; + prompt: string; +} +export namespace CustomAgentDescription { + export function is(entry: unknown): entry is CustomAgentDescription { + // eslint-disable-next-line no-null/no-null + return typeof entry === 'object' && entry !== null + && 'id' in entry && typeof entry.id === 'string' + && 'name' in entry && typeof entry.name === 'string' + && 'description' in entry && typeof entry.description === 'string' + && 'prompt' in entry + && typeof entry.prompt === 'string'; + } + export function equals(a: CustomAgentDescription, b: CustomAgentDescription): boolean { + return a.id === b.id && a.name === b.name && a.description === b.description && a.prompt === b.prompt; + } +} + export const PromptCustomizationService = Symbol('PromptCustomizationService'); export interface PromptCustomizationService { /** @@ -109,6 +130,22 @@ export interface PromptCustomizationService { * Event which is fired when the prompt template is changed. */ readonly onDidChangePrompt: Event; + + /** + * Return all custom agents. + * @returns all custom agents + */ + getCustomAgents(): Promise; + + /** + * Event which is fired when custom agents are modified. + */ + readonly onDidChangeCustomAgents: Event; + + /** + * Open the custom agent yaml file. + */ + openCustomAgentYaml(): void; } @injectable() diff --git a/yarn.lock b/yarn.lock index 049028db042e9..96a371d27655f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2064,6 +2064,11 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== +"@types/js-yaml@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + "@types/jsdom@^21.1.7": version "21.1.7" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.7.tgz#9edcb09e0b07ce876e7833922d3274149c898cfa" @@ -11250,7 +11255,7 @@ string-argv@^0.1.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738" integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11268,6 +11273,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -11333,7 +11347,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11354,6 +11368,13 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -12564,7 +12585,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -12582,6 +12603,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 9a6fffa59ea210488a653a95d751397ddf0e3fec Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Wed, 16 Oct 2024 23:11:52 +0200 Subject: [PATCH 2/5] fix issues found by review: - fix prompt key of custom agent - improve ui - add example entry for a new custom agent file --- packages/ai-chat/src/common/custom-chat-agent.ts | 2 +- .../ai-configuration/agent-configuration-widget.tsx | 4 +++- .../src/browser/frontend-prompt-customization-service.ts | 7 ++++++- packages/ai-core/src/browser/style/index.css | 5 +++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/ai-chat/src/common/custom-chat-agent.ts b/packages/ai-chat/src/common/custom-chat-agent.ts index 5d0cdc90ceb7d..52743d654dba7 100644 --- a/packages/ai-chat/src/common/custom-chat-agent.ts +++ b/packages/ai-chat/src/common/custom-chat-agent.ts @@ -39,6 +39,6 @@ export class CustomChatAgent } set prompt(prompt: string) { - this.promptTemplates.push({ id: `${this.name}_system`, template: prompt }); + this.promptTemplates.push({ id: `${this.name}_prompt`, template: prompt }); } } diff --git a/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx b/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx index 5b2ad05974237..9a8798e584986 100644 --- a/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx +++ b/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx @@ -101,7 +101,9 @@ export class AIAgentConfigurationWidget extends ReactWidget { )} - +
+ +
{this.renderAgentDetails()} diff --git a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts index 27d44a342f8a9..7866519d4fd8c 100644 --- a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts +++ b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts @@ -26,6 +26,11 @@ import { AgentService } from '../common/agent-service'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { load } from 'js-yaml'; +const templateEntry = `-id: my_agent +name: My Agent +description: This is an example agent. Please adapt the properties to fit your needs. +prompt: You are an example agent. Be nice and helpful to the user.`; + @injectable() export class FrontendPromptCustomizationServiceImpl implements PromptCustomizationService { @@ -224,7 +229,7 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati async openCustomAgentYaml(): Promise { const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml'); if (! await this.fileService.exists(customAgentYamlUri)) { - await this.fileService.createFile(customAgentYamlUri); + await this.fileService.createFile(customAgentYamlUri, BinaryBuffer.fromString(templateEntry)); } const openHandler = await this.openerService.getOpener(customAgentYamlUri); openHandler.open(customAgentYamlUri); diff --git a/packages/ai-core/src/browser/style/index.css b/packages/ai-core/src/browser/style/index.css index b325058b20541..36cdad9c19221 100644 --- a/packages/ai-core/src/browser/style/index.css +++ b/packages/ai-core/src/browser/style/index.css @@ -88,3 +88,8 @@ border-radius: calc(var(--theia-ui-padding) * 2 / 3); background: hsla(0, 0%, 68%, 0.31); } + +.configuration-agents-add { + margin-top: 3em; + margin-left: 0; +} From 9692ac387e47af5f4b2674bbc10e9efdcb2bd540 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Fri, 18 Oct 2024 12:25:28 +0200 Subject: [PATCH 3/5] incorporate review comments --- .../src/browser/ai-chat-frontend-module.ts | 10 +++++++--- .../src/browser/custom-agent-factory.ts | 4 ++-- ...agent-frontend-application-contribution.ts | 10 +++++----- .../frontend-prompt-customization-service.ts | 20 +++++++++++++------ packages/ai-core/src/common/prompt-service.ts | 7 +++++-- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/ai-chat/src/browser/ai-chat-frontend-module.ts b/packages/ai-chat/src/browser/ai-chat-frontend-module.ts index cd84c076f6c5f..396bcb7331a06 100644 --- a/packages/ai-chat/src/browser/ai-chat-frontend-module.ts +++ b/packages/ai-chat/src/browser/ai-chat-frontend-module.ts @@ -36,7 +36,7 @@ import { UniversalChatAgent } from '../common/universal-chat-agent'; import { aiChatPreferences } from './ai-chat-preferences'; import { AICustomAgentsFrontendApplicationContribution } from './custom-agent-frontend-application-contribution'; import { FrontendChatServiceImpl } from './frontend-chat-service'; -import { FactoryOfCustomAgents } from './custom-agent-factory'; +import { CustomAgentFactory } from './custom-agent-factory'; export default new ContainerModule(bind => { bindContributionProvider(bind, Agent); @@ -74,13 +74,17 @@ export default new ContainerModule(bind => { bind(PreferenceContribution).toConstantValue({ schema: aiChatPreferences }); bind(CustomChatAgent).toSelf(); - bind(FactoryOfCustomAgents).toFactory( - ctx => (id: string, name: string, description: string, prompt: string) => { + bind(CustomAgentFactory).toFactory( + ctx => (id: string, name: string, description: string, prompt: string, defaultLLM: string) => { const agent = ctx.container.get(CustomChatAgent); agent.id = id; agent.name = name; agent.description = description; agent.prompt = prompt; + agent.languageModelRequirements = [{ + purpose: 'chat', + identifier: defaultLLM, + }]; ctx.container.get(ChatAgentService).registerChatAgent(agent); ctx.container.get(AgentService).registerAgent(agent); return agent; diff --git a/packages/ai-chat/src/browser/custom-agent-factory.ts b/packages/ai-chat/src/browser/custom-agent-factory.ts index dbf94cfa49c2e..9fe67f88e723e 100644 --- a/packages/ai-chat/src/browser/custom-agent-factory.ts +++ b/packages/ai-chat/src/browser/custom-agent-factory.ts @@ -16,5 +16,5 @@ import { CustomChatAgent } from '../common'; -export const FactoryOfCustomAgents = Symbol('FactoryOfCustomAgents'); -export type FactoryOfCustomAgents = (id: string, name: string, description: string, prompt: string) => CustomChatAgent; +export const CustomAgentFactory = Symbol('CustomAgentFactory'); +export type CustomAgentFactory = (id: string, name: string, description: string, prompt: string, defaultLLM: string) => CustomChatAgent; diff --git a/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts b/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts index e6cea408e2866..4c67a14ab508c 100644 --- a/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts +++ b/packages/ai-chat/src/browser/custom-agent-frontend-application-contribution.ts @@ -18,12 +18,12 @@ import { AgentService, CustomAgentDescription, PromptCustomizationService } from import { FrontendApplicationContribution } from '@theia/core/lib/browser'; import { inject, injectable } from '@theia/core/shared/inversify'; import { ChatAgentService } from '../common'; -import { FactoryOfCustomAgents } from './custom-agent-factory'; +import { CustomAgentFactory } from './custom-agent-factory'; @injectable() export class AICustomAgentsFrontendApplicationContribution implements FrontendApplicationContribution { - @inject(FactoryOfCustomAgents) - protected readonly customAgentFactory: FactoryOfCustomAgents; + @inject(CustomAgentFactory) + protected readonly customAgentFactory: CustomAgentFactory; @inject(PromptCustomizationService) protected readonly customizationService: PromptCustomizationService; @@ -38,7 +38,7 @@ export class AICustomAgentsFrontendApplicationContribution implements FrontendAp onStart(): void { this.customizationService?.getCustomAgents().then(customAgents => { customAgents.forEach(agent => { - this.customAgentFactory(agent.id, agent.name, agent.description, agent.prompt); + this.customAgentFactory(agent.id, agent.name, agent.description, agent.prompt, agent.defaultLLM); this.knownCustomAgents.set(agent.id, agent); }); }).catch(e => { @@ -59,7 +59,7 @@ export class AICustomAgentsFrontendApplicationContribution implements FrontendAp }); customAgentsToAdd .forEach(agent => { - this.customAgentFactory(agent.id, agent.name, agent.description, agent.prompt); + this.customAgentFactory(agent.id, agent.name, agent.description, agent.prompt, agent.defaultLLM); this.knownCustomAgents.set(agent.id, agent); }); }).catch(e => { diff --git a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts index 7866519d4fd8c..782d9112e8e16 100644 --- a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts +++ b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts @@ -24,12 +24,15 @@ import { FileChangesEvent } from '@theia/filesystem/lib/common/files'; import { AICorePreferences, PREFERENCE_NAME_PROMPT_TEMPLATES } from './ai-core-preferences'; import { AgentService } from '../common/agent-service'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { load } from 'js-yaml'; +import { load, dump } from 'js-yaml'; -const templateEntry = `-id: my_agent -name: My Agent -description: This is an example agent. Please adapt the properties to fit your needs. -prompt: You are an example agent. Be nice and helpful to the user.`; +const templateEntry = { + id: 'my_agent', + name: 'My Agent', + description: 'This is an example agent. Please adapt the properties to fit your needs.', + prompt: 'You are an example agent. Be nice and helpful to the user.', + defaultLLM: 'openai/gpt-4o' +}; @injectable() export class FrontendPromptCustomizationServiceImpl implements PromptCustomizationService { @@ -115,6 +118,7 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati })); + this.onDidChangeCustomAgentsEmitter.fire(); const stat = await this.fileService.resolve(templateURI); if (stat.children === undefined) { return; @@ -228,8 +232,12 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati async openCustomAgentYaml(): Promise { const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml'); + const content = dump([templateEntry]); if (! await this.fileService.exists(customAgentYamlUri)) { - await this.fileService.createFile(customAgentYamlUri, BinaryBuffer.fromString(templateEntry)); + await this.fileService.createFile(customAgentYamlUri, BinaryBuffer.fromString(content)); + } else { + const fileContent = (await this.fileService.readFile(customAgentYamlUri)).value; + await this.fileService.writeFile(customAgentYamlUri, BinaryBuffer.concat([fileContent, BinaryBuffer.fromString(content)])); } const openHandler = await this.openerService.getOpener(customAgentYamlUri); openHandler.open(customAgentYamlUri); diff --git a/packages/ai-core/src/common/prompt-service.ts b/packages/ai-core/src/common/prompt-service.ts index de6f4446b94ca..44d8fb0622d66 100644 --- a/packages/ai-core/src/common/prompt-service.ts +++ b/packages/ai-core/src/common/prompt-service.ts @@ -74,6 +74,7 @@ export interface CustomAgentDescription { name: string; description: string; prompt: string; + defaultLLM: string; } export namespace CustomAgentDescription { export function is(entry: unknown): entry is CustomAgentDescription { @@ -83,10 +84,12 @@ export namespace CustomAgentDescription { && 'name' in entry && typeof entry.name === 'string' && 'description' in entry && typeof entry.description === 'string' && 'prompt' in entry - && typeof entry.prompt === 'string'; + && typeof entry.prompt === 'string' + && 'defaultLLM' in entry + && typeof entry.defaultLLM === 'string'; } export function equals(a: CustomAgentDescription, b: CustomAgentDescription): boolean { - return a.id === b.id && a.name === b.name && a.description === b.description && a.prompt === b.prompt; + return a.id === b.id && a.name === b.name && a.description === b.description && a.prompt === b.prompt && a.defaultLLM === b.defaultLLM; } } From bf7d7a8189cbbedbd8f430bee8d8d8cee03a3435 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Fri, 18 Oct 2024 14:36:55 +0200 Subject: [PATCH 4/5] add check during read for unique agent identification --- .../frontend-prompt-customization-service.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts index 782d9112e8e16..f9fd49f9a66b9 100644 --- a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts +++ b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts @@ -223,7 +223,18 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati console.debug('Invalid customAgents.yml file content'); return []; } - return doc as CustomAgentDescription[]; + const readAgents = doc as CustomAgentDescription[]; + // make sure all agents are unique (id and name) + const uniqueAgentIds = new Set(); + const uniqueAgens: CustomAgentDescription[] = []; + readAgents.forEach(agent => { + if (uniqueAgentIds.has(`${agent.id}_${agent.name}`)) { + return; + } + uniqueAgentIds.add(`${agent.id}_${agent.name}`); + uniqueAgens.push(agent); + }); + return uniqueAgens; } catch (e) { console.debug(e.message, e); return []; From 22eaa294404ae48b75cf246d81c720cc877c7890 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Fri, 18 Oct 2024 14:44:29 +0200 Subject: [PATCH 5/5] fix to only use id --- .../src/browser/frontend-prompt-customization-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts index f9fd49f9a66b9..11be74b482834 100644 --- a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts +++ b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts @@ -228,10 +228,10 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati const uniqueAgentIds = new Set(); const uniqueAgens: CustomAgentDescription[] = []; readAgents.forEach(agent => { - if (uniqueAgentIds.has(`${agent.id}_${agent.name}`)) { + if (uniqueAgentIds.has(agent.id)) { return; } - uniqueAgentIds.add(`${agent.id}_${agent.name}`); + uniqueAgentIds.add(agent.id); uniqueAgens.push(agent); }); return uniqueAgens;