// ***************************************************************************** // 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 // ***************************************************************************** /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatAgents.ts import { ContributionProvider, Emitter, Event, ILogger, PreferenceService } from '@theia/core'; import { inject, injectable, named, optional, postConstruct } from '@theia/core/shared/inversify'; import { ChatAgent } from './chat-agents'; import { AgentService, AISettingsService } from '@theia/ai-core'; import { ParsedChatRequest, ParsedChatRequestAgentPart } from './parsed-chat-request'; import { DEFAULT_CHAT_AGENT_PREF } from './ai-chat-preferences'; /** * The default chat agent to invoke */ export const DefaultChatAgentId = Symbol('DefaultChatAgentId'); export interface DefaultChatAgentId { id: string; } /** * In case no fitting chat agent is available, this one will be used (if it is itself available) */ export const FallbackChatAgentId = Symbol('FallbackChatAgentId'); export interface FallbackChatAgentId { id: string; } export const ChatAgentService = Symbol('ChatAgentService'); export const ChatAgentServiceFactory = Symbol('ChatAgentServiceFactory'); /** * The ChatAgentService provides access to the available chat agents. */ export interface ChatAgentService { /** * Returns all available agents. * @param includeHidden whether to include agents that are hidden from chat (showInChat set to false). Defaults to false, meaning hidden agents will not be returned. */ getAgents(includeHidden?: boolean): ChatAgent[]; /** * Returns the specified agent, if available * @param id the agent id * @param includeHidden whether to include agents that are hidden from chat (showInChat set to false). Defaults to false, meaning hidden agents will not be returned. */ getAgent(id: string, includeHidden?: boolean): ChatAgent | undefined; /** * 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; /** * Returns the configured default chat agent, if available and enabled. */ getDefaultAgent(): ChatAgent | undefined; /** * Returns the configured fallback chat agent, if available and enabled. */ getFallbackAgent(): ChatAgent | undefined; /** * Resolves the agent from a parsed request. * Checks for explicit @agent mention first, then preference-based default, * then DI-bound default, and finally falls back to the fallback agent. * @param parsedRequest The parsed chat request to resolve the agent from * @returns The resolved agent, or undefined if no agent could be determined */ resolveAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined; /** * Returns the user's preference-configured default chat agent, if available and enabled. * This takes precedence over the DI-bound default agent. */ getPreferenceDefaultAgent(): ChatAgent | undefined; /** * Returns the effective default agent by checking preference first, then DI-bound default. * @returns The effective default agent, or undefined if none is configured or available */ getEffectiveDefaultAgent(): ChatAgent | undefined; /** * Fired when an agent is registered or unregistered. */ readonly onDidChangeAgents: Event; /** * Fired when the effective default agent changes (either through preference change * or when the configured agent becomes available/unavailable). */ readonly onDefaultAgentChanged: Event; } @injectable() export class ChatAgentServiceImpl implements ChatAgentService { protected readonly onDidChangeAgentsEmitter = new Emitter(); readonly onDidChangeAgents: Event = this.onDidChangeAgentsEmitter.event; protected readonly onDefaultAgentChangedEmitter = new Emitter(); readonly onDefaultAgentChanged: Event = this.onDefaultAgentChangedEmitter.event; @inject(ContributionProvider) @named(ChatAgent) protected readonly agentContributions: ContributionProvider; @inject(ILogger) @named('ai-chat:ChatAgentServiceImpl') protected readonly logger: ILogger; @inject(AgentService) protected readonly agentService: AgentService; @inject(PreferenceService) @optional() protected readonly preferenceService: PreferenceService | undefined; @inject(DefaultChatAgentId) @optional() protected readonly defaultChatAgentId: DefaultChatAgentId | undefined; @inject(FallbackChatAgentId) @optional() protected readonly fallbackChatAgentId: FallbackChatAgentId | undefined; @inject(AISettingsService) @optional() protected readonly aiSettingsService: AISettingsService | undefined; protected _agents: ChatAgent[] = []; protected hiddenFromChatAgents = new Set(); protected _cachedPreferenceDefaultAgentId: string | undefined; @postConstruct() protected init(): void { this.aiSettingsService?.getSettings().then(settings => { Object.entries(settings).forEach(([agentId, agentSettings]) => { if (agentSettings.showInChat === false) { this.hiddenFromChatAgents.add(agentId); } }); }); this.aiSettingsService?.onDidChange(() => { this.aiSettingsService?.getSettings().then(settings => { this.hiddenFromChatAgents.clear(); Object.entries(settings).forEach(([agentId, agentSettings]) => { if (agentSettings.showInChat === false) { this.hiddenFromChatAgents.add(agentId); } }); }); }); if (this.preferenceService) { this.preferenceService.onPreferenceChanged(event => { if (event.preferenceName === DEFAULT_CHAT_AGENT_PREF) { this.handleDefaultAgentPreferenceChange(); } }); } // Also fire when agents change, as the configured default might become available/unavailable this.onDidChangeAgents(() => { this.checkAndFireDefaultAgentChange(); }); } protected handleDefaultAgentPreferenceChange(): void { const newAgentId = this.preferenceService?.get(DEFAULT_CHAT_AGENT_PREF, undefined); if (newAgentId !== this._cachedPreferenceDefaultAgentId) { this._cachedPreferenceDefaultAgentId = newAgentId; this.onDefaultAgentChangedEmitter.fire(this.getEffectiveDefaultAgent()); } } protected checkAndFireDefaultAgentChange(): void { // Fire event when agents change, as this may affect the effective default this.onDefaultAgentChangedEmitter.fire(this.getEffectiveDefaultAgent()); } protected get agents(): ChatAgent[] { // We can't collect the contributions at @postConstruct because this will lead to a circular dependency // with chat agents reusing the chat agent service (e.g. orchestrator) return [...this.agentContributions.getContributions(), ...this._agents]; } registerChatAgent(agent: ChatAgent): void { this._agents.push(agent); this.onDidChangeAgentsEmitter.fire(); } unregisterChatAgent(agentId: string): void { this._agents = this._agents.filter(a => a.id !== agentId); this.onDidChangeAgentsEmitter.fire(); } getAgent(id: string, includeHidden: boolean = false): ChatAgent | undefined { if (!this._agentIsEnabled(id)) { return undefined; } return this.getAgents(includeHidden).find(agent => agent.id === id); } getAgents(includeHidden: boolean = false): ChatAgent[] { return this.agents.filter(a => this._agentIsEnabled(a.id) && (includeHidden || this._agentShowsInChat(a.id))); } getAllAgents(): ChatAgent[] { return this.agents; } private _agentIsEnabled(id: string): boolean { return this.agentService.isEnabled(id); } private _agentShowsInChat(id: string): boolean { return !this.hiddenFromChatAgents.has(id); } getDefaultAgent(): ChatAgent | undefined { if (this.defaultChatAgentId) { return this.getAgent(this.defaultChatAgentId.id); } return undefined; } getFallbackAgent(): ChatAgent | undefined { if (this.fallbackChatAgentId) { return this.getAgent(this.fallbackChatAgentId.id); } return undefined; } getPreferenceDefaultAgent(): ChatAgent | undefined { if (!this.preferenceService) { return undefined; } const configuredAgentId = this.preferenceService.get(DEFAULT_CHAT_AGENT_PREF, undefined); if (configuredAgentId) { const agent = this.getAgent(configuredAgentId); if (!agent) { this.logger.warn(`The configured default chat agent with id '${configuredAgentId}' does not exist or is disabled.`); } return agent; } return undefined; } getEffectiveDefaultAgent(): ChatAgent | undefined { // Check preference-based default first const preferenceDefault = this.getPreferenceDefaultAgent(); if (preferenceDefault) { return preferenceDefault; } // Fall back to DI-bound default return this.getDefaultAgent(); } resolveAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined { // First, check for explicitly mentioned agent const agentPart = parsedRequest.parts.find( (p): p is ParsedChatRequestAgentPart => p instanceof ParsedChatRequestAgentPart ); if (agentPart) { return this.getAgent(agentPart.agentId); } // Fall back to effective default agent (preference > DI-bound) const effectiveDefault = this.getEffectiveDefaultAgent(); if (effectiveDefault) { return effectiveDefault; } // Fall back to fallback agent return this.getFallbackAgent(); } }