Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/chatParticipants.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import * as vscode from 'vscode';
6
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
7
import { IChatAgentService, defaultAgentName, editingSessionAgentEditorName, editingSessionAgentName, editsAgentName, getChatParticipantIdFromName, notebookEditorAgentName, terminalAgentName, vscodeAgentName } from '../../../platform/chat/common/chatAgents';
8
import { IChatQuotaService } from '../../../platform/chat/common/chatQuotaService';
9
import { IChatSessionService } from '../../../platform/chat/common/chatSessionService';
10
import { IInteractionService } from '../../../platform/chat/common/interactionService';
11
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
12
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
13
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
14
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
15
import { ChatExtPerfMark, clearChatExtMarks, markChatExt } from '../../../util/common/performance';
16
import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
17
import { autorun } from '../../../util/vs/base/common/observableInternal';
18
import { generateUuid } from '../../../util/vs/base/common/uuid';
19
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
20
import { ChatRequest } from '../../../vscodeTypes';
21
import { Intent, agentsToCommands } from '../../common/constants';
22
import { ICopilotChatResultIn } from '../../prompt/common/conversation';
23
import { getSwitchToAutoOnRateLimitConfirmation, isContinueOnError } from '../../prompt/common/specialRequestTypes';
24
import { ChatParticipantRequestHandler } from '../../prompt/node/chatParticipantRequestHandler';
25
import { IFeedbackReporter } from '../../prompt/node/feedbackReporter';
26
import { IPromptCategorizerService } from '../../prompt/node/promptCategorizer';
27
import { ChatSummarizerProvider } from '../../prompt/node/summarizer';
28
import { ChatTitleProvider } from '../../prompt/node/title';
29
import { IUserFeedbackService } from './userActions';
30
import { getAdditionalWelcomeMessage } from './welcomeMessageProvider';
31
32
export class ChatAgentService implements IChatAgentService {
33
declare readonly _serviceBrand: undefined;
34
35
private _lastChatAgents: ChatAgents | undefined; // will be cleared when disposed
36
37
constructor(
38
@IInstantiationService private readonly instantiationService: IInstantiationService,
39
) { }
40
public debugGetCurrentChatAgents(): ChatAgents | undefined {
41
return this._lastChatAgents;
42
}
43
44
register(): IDisposable {
45
const chatAgents = this.instantiationService.createInstance(ChatAgents);
46
chatAgents.register();
47
this._lastChatAgents = chatAgents;
48
return {
49
dispose: () => {
50
chatAgents.dispose();
51
this._lastChatAgents = undefined;
52
}
53
};
54
}
55
}
56
57
class ChatAgents implements IDisposable {
58
private readonly _disposables = new DisposableStore();
59
60
private additionalWelcomeMessage: vscode.MarkdownString | undefined;
61
62
constructor(
63
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
64
@IInstantiationService private readonly instantiationService: IInstantiationService,
65
@IUserFeedbackService private readonly userFeedbackService: IUserFeedbackService,
66
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
67
@IFeedbackReporter private readonly feedbackReporter: IFeedbackReporter,
68
@IInteractionService private readonly interactionService: IInteractionService,
69
@IChatQuotaService private readonly _chatQuotaService: IChatQuotaService,
70
@IConfigurationService private readonly configurationService: IConfigurationService,
71
@IExperimentationService private readonly experimentationService: IExperimentationService,
72
@IPromptCategorizerService private readonly promptCategorizerService: IPromptCategorizerService,
73
@ITelemetryService private readonly telemetryService: ITelemetryService,
74
@IChatSessionService chatSessionService: IChatSessionService,
75
) {
76
this._disposables.add(chatSessionService.onDidDisposeChatSession(sessionId => clearChatExtMarks(sessionId)));
77
}
78
79
dispose() {
80
this._disposables.dispose();
81
}
82
83
register(): void {
84
this.additionalWelcomeMessage = this.instantiationService.invokeFunction(getAdditionalWelcomeMessage);
85
this._disposables.add(this.registerDefaultAgent());
86
this._disposables.add(this.registerEditingAgent());
87
this._disposables.add(this.registerEditingAgentEditor());
88
this._disposables.add(this.registerEditsAgent());
89
this._disposables.add(this.registerNotebookEditorDefaultAgent());
90
this._disposables.add(this.registerNotebookDefaultAgent());
91
this._disposables.add(this.registerVSCodeAgent());
92
this._disposables.add(this.registerTerminalAgent());
93
this._disposables.add(this.registerTerminalPanelAgent());
94
}
95
96
private createAgent(name: string, defaultIntentIdOrGetter: IntentOrGetter, options?: { id?: string }): vscode.ChatParticipant {
97
const id = options?.id || getChatParticipantIdFromName(name);
98
const agent = vscode.chat.createChatParticipant(id, this.getChatParticipantHandler(id, name, defaultIntentIdOrGetter));
99
agent.onDidReceiveFeedback(e => {
100
this.userFeedbackService.handleFeedback(e, id);
101
});
102
agent.onDidPerformAction(e => {
103
this.userFeedbackService.handleUserAction(e, id);
104
});
105
this._disposables.add(autorun(reader => {
106
agent.supportIssueReporting = this.feedbackReporter.canReport.read(reader);
107
}));
108
109
return agent;
110
}
111
112
private registerVSCodeAgent(): IDisposable {
113
const useInsidersIcon = vscode.env.appName.includes('Insiders') || vscode.env.appName.includes('OSS');
114
const vscodeAgent = this.createAgent(vscodeAgentName, Intent.VSCode);
115
vscodeAgent.iconPath = useInsidersIcon ? new vscode.ThemeIcon('vscode-insiders') : new vscode.ThemeIcon('vscode');
116
return vscodeAgent;
117
}
118
119
private registerTerminalAgent(): IDisposable {
120
const terminalAgent = this.createAgent(terminalAgentName, Intent.Terminal);
121
122
terminalAgent.iconPath = new vscode.ThemeIcon('terminal');
123
return terminalAgent;
124
}
125
126
private registerTerminalPanelAgent(): IDisposable {
127
const terminalPanelAgent = this.createAgent(terminalAgentName, Intent.Terminal, { id: 'github.copilot.terminalPanel' });
128
129
terminalPanelAgent.iconPath = new vscode.ThemeIcon('terminal');
130
131
return terminalPanelAgent;
132
}
133
134
private registerEditingAgent(): IDisposable {
135
const editingAgent = this.createAgent(editingSessionAgentName, Intent.Edit);
136
editingAgent.iconPath = new vscode.ThemeIcon('copilot');
137
editingAgent.additionalWelcomeMessage = this.additionalWelcomeMessage;
138
editingAgent.titleProvider = this.instantiationService.createInstance(ChatTitleProvider);
139
return editingAgent;
140
}
141
142
private registerEditingAgentEditor(): IDisposable {
143
const editingAgent = this.createAgent(editingSessionAgentEditorName, Intent.InlineChat);
144
editingAgent.iconPath = new vscode.ThemeIcon('copilot');
145
return editingAgent;
146
}
147
148
private registerEditsAgent(): IDisposable {
149
const editingAgent = this.createAgent(editsAgentName, Intent.Agent);
150
editingAgent.iconPath = new vscode.ThemeIcon('tools');
151
editingAgent.additionalWelcomeMessage = this.additionalWelcomeMessage;
152
editingAgent.titleProvider = this.instantiationService.createInstance(ChatTitleProvider);
153
return editingAgent;
154
}
155
156
private registerDefaultAgent(): IDisposable {
157
const intentGetter = (request: vscode.ChatRequest) => {
158
if (this.configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AskAgent, this.experimentationService) && request.model.capabilities.supportsToolCalling && this.configurationService.getNonExtensionConfig('chat.agent.enabled')) {
159
return Intent.AskAgent;
160
}
161
return Intent.Unknown;
162
};
163
const defaultAgent = this.createAgent(defaultAgentName, intentGetter);
164
defaultAgent.iconPath = new vscode.ThemeIcon('copilot');
165
166
defaultAgent.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:');
167
const helpPostfix = vscode.l10n.t({
168
message: `To have a great conversation, ask me questions as if I was a real programmer:
169
170
* **Show me the code** you want to talk about by having the files open and selecting the most important lines.
171
* **Make refinements** by asking me follow-up questions, adding clarifications, providing errors, etc.
172
* **Review my suggested code** and tell me about issues or improvements, so I can iterate on it.
173
174
You can also ask me questions about your editor selection by [starting an inline chat session](command:inlineChat.start).
175
176
Learn 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).`,
177
comment: `{Locked='](command:inlineChat.start)'}`
178
});
179
const markdownString = new vscode.MarkdownString(helpPostfix);
180
markdownString.isTrusted = { enabledCommands: ['inlineChat.start', 'github.copilot.open.walkthrough'] };
181
defaultAgent.helpTextPostfix = markdownString;
182
183
defaultAgent.additionalWelcomeMessage = this.additionalWelcomeMessage;
184
defaultAgent.titleProvider = this.instantiationService.createInstance(ChatTitleProvider);
185
defaultAgent.summarizer = this.instantiationService.createInstance(ChatSummarizerProvider);
186
187
return defaultAgent;
188
}
189
190
private registerNotebookEditorDefaultAgent(): IDisposable {
191
const defaultAgent = this.createAgent('notebook', Intent.Editor);
192
defaultAgent.iconPath = new vscode.ThemeIcon('copilot');
193
194
return defaultAgent;
195
}
196
197
private registerNotebookDefaultAgent(): IDisposable {
198
const defaultAgent = this.createAgent(notebookEditorAgentName, Intent.notebookEditor);
199
defaultAgent.iconPath = new vscode.ThemeIcon('copilot');
200
201
return defaultAgent;
202
}
203
204
private getChatParticipantHandler(id: string, name: string, defaultIntentIdOrGetter: IntentOrGetter): vscode.ChatExtendedRequestHandler {
205
return async (request, context, stream, token): Promise<vscode.ChatResult> => {
206
markChatExt(request.sessionId, ChatExtPerfMark.WillHandleParticipant);
207
try {
208
// If we need to switch to the base model, this function will handle it
209
// Otherwise it just returns the same request passed into it
210
request = await this.switchToBaseModel(request, stream);
211
212
// Handle switch-to-auto confirmation button clicks from rate limit errors
213
const switchToAutoConfirmation = getSwitchToAutoOnRateLimitConfirmation(request);
214
if (switchToAutoConfirmation) {
215
const action = switchToAutoConfirmation.alwaysSwitchToAuto ? 'switchToAutoAlways' : 'switchToAuto';
216
/* __GDPR__
217
"chatRateLimitAction" : {
218
"owner": "lramos15",
219
"comment": "Tracks which action users take when rate limited",
220
"action": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The action taken: switchToAuto, switchToAutoAlways, tryAgain, or autoSwitch." },
221
"modelId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model ID the user was rate limited on." }
222
}
223
*/
224
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action, modelId: request.model?.id });
225
request = await this.switchToAutoModel(request, stream, switchToAutoConfirmation.alwaysSwitchToAuto);
226
} else if (isContinueOnError(request)) {
227
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action: 'tryAgain', modelId: request.model?.id });
228
}
229
230
// The user is starting an interaction with the chat
231
if (!request.subAgentInvocationId) {
232
this.interactionService.startInteraction();
233
}
234
235
// Generate a shared telemetry message ID on the first turn only — subsequent turns have no
236
// categorization event to join and ChatTelemetryBuilder will generate its own ID.
237
const telemetryMessageId = context.history.length === 0 ? generateUuid() : undefined;
238
239
// Categorize the first prompt (fire-and-forget)
240
if (telemetryMessageId !== undefined) {
241
this.promptCategorizerService.categorizePrompt(request, context, telemetryMessageId);
242
}
243
244
const defaultIntentId = typeof defaultIntentIdOrGetter === 'function' ?
245
defaultIntentIdOrGetter(request) :
246
defaultIntentIdOrGetter;
247
248
// empty chatAgentArgs will force InteractiveSession to not use a command or try to parse one out of the query
249
const commandsForAgent = agentsToCommands[defaultIntentId];
250
const intentId = request.command && commandsForAgent ?
251
commandsForAgent[request.command] :
252
defaultIntentId;
253
254
const handler = this.instantiationService.createInstance(ChatParticipantRequestHandler, context.history, request, stream, token, { agentName: name, agentId: id, intentId }, () => context.yieldRequested, telemetryMessageId);
255
256
let result = await handler.getResult();
257
258
// Auto-retry with Auto model when the setting is enabled and the handler signals it
259
if ((result as ICopilotChatResultIn).metadata?.shouldAutoSwitchToAuto) {
260
const previousModelId = request.model?.id;
261
const switchedRequest = await this.switchToAutoModel(request, stream, false);
262
if (switchedRequest.model?.id !== previousModelId) {
263
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action: 'autoSwitch', modelId: previousModelId });
264
request = switchedRequest;
265
const retryHandler = this.instantiationService.createInstance(ChatParticipantRequestHandler, context.history, request, stream, token, { agentName: name, agentId: id, intentId }, () => context.yieldRequested, telemetryMessageId);
266
result = await retryHandler.getResult();
267
}
268
}
269
270
return result;
271
} finally {
272
markChatExt(request.sessionId, ChatExtPerfMark.DidHandleParticipant);
273
clearChatExtMarks(request.sessionId);
274
}
275
};
276
}
277
278
private async switchToBaseModel(request: vscode.ChatRequest, stream: vscode.ChatResponseStream): Promise<ChatRequest> {
279
const endpoint = await this.endpointProvider.getChatEndpoint(request);
280
const baseEndpoint = await this.endpointProvider.getChatEndpoint('copilot-base');
281
// 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.
282
if (endpoint.multiplier === 0 || request.model.vendor !== 'copilot' || endpoint.multiplier === undefined) {
283
return request;
284
}
285
if (this._chatQuotaService.additionalUsageEnabled || !this._chatQuotaService.quotaExhausted) {
286
return request;
287
}
288
const baseLmModel = (await vscode.lm.selectChatModels({ id: baseEndpoint.model, family: baseEndpoint.family, vendor: 'copilot' }))[0];
289
if (!baseLmModel) {
290
return request;
291
}
292
await vscode.commands.executeCommand('workbench.action.chat.changeModel', { vendor: baseLmModel.vendor, id: baseLmModel.id, family: baseLmModel.family });
293
// Switch to the base model and show a warning
294
request = { ...request, model: baseLmModel };
295
let messageString: vscode.MarkdownString;
296
if (this.authenticationService.copilotToken?.isIndividual) {
297
messageString = new vscode.MarkdownString(vscode.l10n.t({
298
message: '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.',
299
args: [baseEndpoint.name, 'command:chat.enableAdditionalUsage'],
300
// To make sure the translators don't break the link
301
comment: [`{Locked=']({'}`]
302
}));
303
messageString.isTrusted = { enabledCommands: ['chat.enableAdditionalUsage'] };
304
} else {
305
messageString = 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));
306
}
307
stream.warning(messageString);
308
return request;
309
}
310
311
private async switchToAutoModel(request: vscode.ChatRequest, stream: vscode.ChatResponseStream, alwaysSwitchToAuto: boolean): Promise<ChatRequest> {
312
const autoModel = (await vscode.lm.selectChatModels({ id: 'auto', vendor: 'copilot' }))[0];
313
if (!autoModel) {
314
return request;
315
}
316
await vscode.commands.executeCommand('workbench.action.chat.changeModel', { vendor: autoModel.vendor, id: autoModel.id, family: autoModel.family });
317
request = { ...request, model: autoModel };
318
if (alwaysSwitchToAuto) {
319
await vscode.workspace.getConfiguration('github.copilot').update('chat.rateLimitAutoSwitchToAuto', true, vscode.ConfigurationTarget.Global);
320
}
321
stream.warning(new vscode.MarkdownString(vscode.l10n.t('You were rate-limited on the selected model. Switching to Auto and retrying your request.')));
322
return request;
323
}
324
325
}
326
327
type IntentOrGetter = Intent | ((request: vscode.ChatRequest) => Intent);
328
329