Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/chatParticipants.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as vscode from 'vscode';5import { IAuthenticationService } from '../../../platform/authentication/common/authentication';6import { IChatAgentService, defaultAgentName, editingSessionAgentEditorName, editingSessionAgentName, editsAgentName, getChatParticipantIdFromName, notebookEditorAgentName, terminalAgentName, vscodeAgentName } from '../../../platform/chat/common/chatAgents';7import { IChatQuotaService } from '../../../platform/chat/common/chatQuotaService';8import { IChatSessionService } from '../../../platform/chat/common/chatSessionService';9import { IInteractionService } from '../../../platform/chat/common/interactionService';10import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';11import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';12import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';13import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';14import { ChatExtPerfMark, clearChatExtMarks, markChatExt } from '../../../util/common/performance';15import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';16import { autorun } from '../../../util/vs/base/common/observableInternal';17import { generateUuid } from '../../../util/vs/base/common/uuid';18import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';19import { ChatRequest } from '../../../vscodeTypes';20import { Intent, agentsToCommands } from '../../common/constants';21import { ICopilotChatResultIn } from '../../prompt/common/conversation';22import { getSwitchToAutoOnRateLimitConfirmation, isContinueOnError } from '../../prompt/common/specialRequestTypes';23import { ChatParticipantRequestHandler } from '../../prompt/node/chatParticipantRequestHandler';24import { IFeedbackReporter } from '../../prompt/node/feedbackReporter';25import { IPromptCategorizerService } from '../../prompt/node/promptCategorizer';26import { ChatSummarizerProvider } from '../../prompt/node/summarizer';27import { ChatTitleProvider } from '../../prompt/node/title';28import { IUserFeedbackService } from './userActions';29import { getAdditionalWelcomeMessage } from './welcomeMessageProvider';3031export class ChatAgentService implements IChatAgentService {32declare readonly _serviceBrand: undefined;3334private _lastChatAgents: ChatAgents | undefined; // will be cleared when disposed3536constructor(37@IInstantiationService private readonly instantiationService: IInstantiationService,38) { }39public debugGetCurrentChatAgents(): ChatAgents | undefined {40return this._lastChatAgents;41}4243register(): IDisposable {44const chatAgents = this.instantiationService.createInstance(ChatAgents);45chatAgents.register();46this._lastChatAgents = chatAgents;47return {48dispose: () => {49chatAgents.dispose();50this._lastChatAgents = undefined;51}52};53}54}5556class ChatAgents implements IDisposable {57private readonly _disposables = new DisposableStore();5859private additionalWelcomeMessage: vscode.MarkdownString | undefined;6061constructor(62@IAuthenticationService private readonly authenticationService: IAuthenticationService,63@IInstantiationService private readonly instantiationService: IInstantiationService,64@IUserFeedbackService private readonly userFeedbackService: IUserFeedbackService,65@IEndpointProvider private readonly endpointProvider: IEndpointProvider,66@IFeedbackReporter private readonly feedbackReporter: IFeedbackReporter,67@IInteractionService private readonly interactionService: IInteractionService,68@IChatQuotaService private readonly _chatQuotaService: IChatQuotaService,69@IConfigurationService private readonly configurationService: IConfigurationService,70@IExperimentationService private readonly experimentationService: IExperimentationService,71@IPromptCategorizerService private readonly promptCategorizerService: IPromptCategorizerService,72@ITelemetryService private readonly telemetryService: ITelemetryService,73@IChatSessionService chatSessionService: IChatSessionService,74) {75this._disposables.add(chatSessionService.onDidDisposeChatSession(sessionId => clearChatExtMarks(sessionId)));76}7778dispose() {79this._disposables.dispose();80}8182register(): void {83this.additionalWelcomeMessage = this.instantiationService.invokeFunction(getAdditionalWelcomeMessage);84this._disposables.add(this.registerDefaultAgent());85this._disposables.add(this.registerEditingAgent());86this._disposables.add(this.registerEditingAgentEditor());87this._disposables.add(this.registerEditsAgent());88this._disposables.add(this.registerNotebookEditorDefaultAgent());89this._disposables.add(this.registerNotebookDefaultAgent());90this._disposables.add(this.registerVSCodeAgent());91this._disposables.add(this.registerTerminalAgent());92this._disposables.add(this.registerTerminalPanelAgent());93}9495private createAgent(name: string, defaultIntentIdOrGetter: IntentOrGetter, options?: { id?: string }): vscode.ChatParticipant {96const id = options?.id || getChatParticipantIdFromName(name);97const agent = vscode.chat.createChatParticipant(id, this.getChatParticipantHandler(id, name, defaultIntentIdOrGetter));98agent.onDidReceiveFeedback(e => {99this.userFeedbackService.handleFeedback(e, id);100});101agent.onDidPerformAction(e => {102this.userFeedbackService.handleUserAction(e, id);103});104this._disposables.add(autorun(reader => {105agent.supportIssueReporting = this.feedbackReporter.canReport.read(reader);106}));107108return agent;109}110111private registerVSCodeAgent(): IDisposable {112const useInsidersIcon = vscode.env.appName.includes('Insiders') || vscode.env.appName.includes('OSS');113const vscodeAgent = this.createAgent(vscodeAgentName, Intent.VSCode);114vscodeAgent.iconPath = useInsidersIcon ? new vscode.ThemeIcon('vscode-insiders') : new vscode.ThemeIcon('vscode');115return vscodeAgent;116}117118private registerTerminalAgent(): IDisposable {119const terminalAgent = this.createAgent(terminalAgentName, Intent.Terminal);120121terminalAgent.iconPath = new vscode.ThemeIcon('terminal');122return terminalAgent;123}124125private registerTerminalPanelAgent(): IDisposable {126const terminalPanelAgent = this.createAgent(terminalAgentName, Intent.Terminal, { id: 'github.copilot.terminalPanel' });127128terminalPanelAgent.iconPath = new vscode.ThemeIcon('terminal');129130return terminalPanelAgent;131}132133private registerEditingAgent(): IDisposable {134const editingAgent = this.createAgent(editingSessionAgentName, Intent.Edit);135editingAgent.iconPath = new vscode.ThemeIcon('copilot');136editingAgent.additionalWelcomeMessage = this.additionalWelcomeMessage;137editingAgent.titleProvider = this.instantiationService.createInstance(ChatTitleProvider);138return editingAgent;139}140141private registerEditingAgentEditor(): IDisposable {142const editingAgent = this.createAgent(editingSessionAgentEditorName, Intent.InlineChat);143editingAgent.iconPath = new vscode.ThemeIcon('copilot');144return editingAgent;145}146147private registerEditsAgent(): IDisposable {148const editingAgent = this.createAgent(editsAgentName, Intent.Agent);149editingAgent.iconPath = new vscode.ThemeIcon('tools');150editingAgent.additionalWelcomeMessage = this.additionalWelcomeMessage;151editingAgent.titleProvider = this.instantiationService.createInstance(ChatTitleProvider);152return editingAgent;153}154155private registerDefaultAgent(): IDisposable {156const intentGetter = (request: vscode.ChatRequest) => {157if (this.configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AskAgent, this.experimentationService) && request.model.capabilities.supportsToolCalling && this.configurationService.getNonExtensionConfig('chat.agent.enabled')) {158return Intent.AskAgent;159}160return Intent.Unknown;161};162const defaultAgent = this.createAgent(defaultAgentName, intentGetter);163defaultAgent.iconPath = new vscode.ThemeIcon('copilot');164165defaultAgent.helpTextPrefix = vscode.l10n.t('You can ask me general programming questions, or chat with the following participants which have specialized expertise and can perform actions:');166const helpPostfix = vscode.l10n.t({167message: `To have a great conversation, ask me questions as if I was a real programmer:168169* **Show me the code** you want to talk about by having the files open and selecting the most important lines.170* **Make refinements** by asking me follow-up questions, adding clarifications, providing errors, etc.171* **Review my suggested code** and tell me about issues or improvements, so I can iterate on it.172173You can also ask me questions about your editor selection by [starting an inline chat session](command:inlineChat.start).174175Learn more about [GitHub Copilot](https://docs.github.com/copilot/using-github-copilot/getting-started-with-github-copilot?tool=vscode&utm_source=editor&utm_medium=chat-panel&utm_campaign=2024q3-em-MSFT-getstarted) in [Visual Studio Code](https://code.visualstudio.com/docs/copilot/overview). Or explore the [Copilot walkthrough](command:github.copilot.open.walkthrough).`,176comment: `{Locked='](command:inlineChat.start)'}`177});178const markdownString = new vscode.MarkdownString(helpPostfix);179markdownString.isTrusted = { enabledCommands: ['inlineChat.start', 'github.copilot.open.walkthrough'] };180defaultAgent.helpTextPostfix = markdownString;181182defaultAgent.additionalWelcomeMessage = this.additionalWelcomeMessage;183defaultAgent.titleProvider = this.instantiationService.createInstance(ChatTitleProvider);184defaultAgent.summarizer = this.instantiationService.createInstance(ChatSummarizerProvider);185186return defaultAgent;187}188189private registerNotebookEditorDefaultAgent(): IDisposable {190const defaultAgent = this.createAgent('notebook', Intent.Editor);191defaultAgent.iconPath = new vscode.ThemeIcon('copilot');192193return defaultAgent;194}195196private registerNotebookDefaultAgent(): IDisposable {197const defaultAgent = this.createAgent(notebookEditorAgentName, Intent.notebookEditor);198defaultAgent.iconPath = new vscode.ThemeIcon('copilot');199200return defaultAgent;201}202203private getChatParticipantHandler(id: string, name: string, defaultIntentIdOrGetter: IntentOrGetter): vscode.ChatExtendedRequestHandler {204return async (request, context, stream, token): Promise<vscode.ChatResult> => {205markChatExt(request.sessionId, ChatExtPerfMark.WillHandleParticipant);206try {207// If we need to switch to the base model, this function will handle it208// Otherwise it just returns the same request passed into it209request = await this.switchToBaseModel(request, stream);210211// Handle switch-to-auto confirmation button clicks from rate limit errors212const switchToAutoConfirmation = getSwitchToAutoOnRateLimitConfirmation(request);213if (switchToAutoConfirmation) {214const action = switchToAutoConfirmation.alwaysSwitchToAuto ? 'switchToAutoAlways' : 'switchToAuto';215/* __GDPR__216"chatRateLimitAction" : {217"owner": "lramos15",218"comment": "Tracks which action users take when rate limited",219"action": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The action taken: switchToAuto, switchToAutoAlways, tryAgain, or autoSwitch." },220"modelId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model ID the user was rate limited on." }221}222*/223this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action, modelId: request.model?.id });224request = await this.switchToAutoModel(request, stream, switchToAutoConfirmation.alwaysSwitchToAuto);225} else if (isContinueOnError(request)) {226this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action: 'tryAgain', modelId: request.model?.id });227}228229// The user is starting an interaction with the chat230if (!request.subAgentInvocationId) {231this.interactionService.startInteraction();232}233234// Generate a shared telemetry message ID on the first turn only — subsequent turns have no235// categorization event to join and ChatTelemetryBuilder will generate its own ID.236const telemetryMessageId = context.history.length === 0 ? generateUuid() : undefined;237238// Categorize the first prompt (fire-and-forget)239if (telemetryMessageId !== undefined) {240this.promptCategorizerService.categorizePrompt(request, context, telemetryMessageId);241}242243const defaultIntentId = typeof defaultIntentIdOrGetter === 'function' ?244defaultIntentIdOrGetter(request) :245defaultIntentIdOrGetter;246247// empty chatAgentArgs will force InteractiveSession to not use a command or try to parse one out of the query248const commandsForAgent = agentsToCommands[defaultIntentId];249const intentId = request.command && commandsForAgent ?250commandsForAgent[request.command] :251defaultIntentId;252253const handler = this.instantiationService.createInstance(ChatParticipantRequestHandler, context.history, request, stream, token, { agentName: name, agentId: id, intentId }, () => context.yieldRequested, telemetryMessageId);254255let result = await handler.getResult();256257// Auto-retry with Auto model when the setting is enabled and the handler signals it258if ((result as ICopilotChatResultIn).metadata?.shouldAutoSwitchToAuto) {259const previousModelId = request.model?.id;260const switchedRequest = await this.switchToAutoModel(request, stream, false);261if (switchedRequest.model?.id !== previousModelId) {262this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action: 'autoSwitch', modelId: previousModelId });263request = switchedRequest;264const retryHandler = this.instantiationService.createInstance(ChatParticipantRequestHandler, context.history, request, stream, token, { agentName: name, agentId: id, intentId }, () => context.yieldRequested, telemetryMessageId);265result = await retryHandler.getResult();266}267}268269return result;270} finally {271markChatExt(request.sessionId, ChatExtPerfMark.DidHandleParticipant);272clearChatExtMarks(request.sessionId);273}274};275}276277private async switchToBaseModel(request: vscode.ChatRequest, stream: vscode.ChatResponseStream): Promise<ChatRequest> {278const endpoint = await this.endpointProvider.getChatEndpoint(request);279const baseEndpoint = await this.endpointProvider.getChatEndpoint('copilot-base');280// If it has a 0x multipler, it's free so don't switch them. If it's BYOK, it's free so don't switch them.281if (endpoint.multiplier === 0 || request.model.vendor !== 'copilot' || endpoint.multiplier === undefined) {282return request;283}284if (this._chatQuotaService.additionalUsageEnabled || !this._chatQuotaService.quotaExhausted) {285return request;286}287const baseLmModel = (await vscode.lm.selectChatModels({ id: baseEndpoint.model, family: baseEndpoint.family, vendor: 'copilot' }))[0];288if (!baseLmModel) {289return request;290}291await vscode.commands.executeCommand('workbench.action.chat.changeModel', { vendor: baseLmModel.vendor, id: baseLmModel.id, family: baseLmModel.family });292// Switch to the base model and show a warning293request = { ...request, model: baseLmModel };294let messageString: vscode.MarkdownString;295if (this.authenticationService.copilotToken?.isIndividual) {296messageString = new vscode.MarkdownString(vscode.l10n.t({297message: 'You have reached your additional usage limit for this month. We have automatically switched you to {0} which is included with your plan. [Configure additional spend]({1}) to keep going.',298args: [baseEndpoint.name, 'command:chat.enableAdditionalUsage'],299// To make sure the translators don't break the link300comment: [`{Locked=']({'}`]301}));302messageString.isTrusted = { enabledCommands: ['chat.enableAdditionalUsage'] };303} else {304messageString = new vscode.MarkdownString(vscode.l10n.t('You have reached your additional usage limit for this month. We have automatically switched you to {0} which is included with your plan. To configure additional spend, contact your organization admin.', baseEndpoint.name));305}306stream.warning(messageString);307return request;308}309310private async switchToAutoModel(request: vscode.ChatRequest, stream: vscode.ChatResponseStream, alwaysSwitchToAuto: boolean): Promise<ChatRequest> {311const autoModel = (await vscode.lm.selectChatModels({ id: 'auto', vendor: 'copilot' }))[0];312if (!autoModel) {313return request;314}315await vscode.commands.executeCommand('workbench.action.chat.changeModel', { vendor: autoModel.vendor, id: autoModel.id, family: autoModel.family });316request = { ...request, model: autoModel };317if (alwaysSwitchToAuto) {318await vscode.workspace.getConfiguration('github.copilot').update('chat.rateLimitAutoSwitchToAuto', true, vscode.ConfigurationTarget.Global);319}320stream.warning(new vscode.MarkdownString(vscode.l10n.t('You were rate-limited on the selected model. Switching to Auto and retrying your request.')));321return request;322}323324}325326type IntentOrGetter = Intent | ((request: vscode.ChatRequest) => Intent);327328329