Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/node/chatParticipantRequestHandler.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
6
import * as l10n from '@vscode/l10n';
7
import type { ChatRequest, ChatRequestTurn2, ChatResponseStream, ChatResult, Location } from 'vscode';
8
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
9
import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade';
10
import { getChatParticipantNameFromId } from '../../../platform/chat/common/chatAgents';
11
import { CanceledMessage, ChatLocation } from '../../../platform/chat/common/commonTypes';
12
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
13
import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';
14
import { ILogService } from '../../../platform/log/common/logService';
15
import { FilterReason } from '../../../platform/networking/common/openai';
16
import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEditorsService';
17
import { getWorkspaceFileDisplayPath, IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
18
import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl';
19
import { fileTreePartToMarkdown } from '../../../util/common/fileTree';
20
import { isLocation, isSymbolInformation } from '../../../util/common/types';
21
import { coalesce } from '../../../util/vs/base/common/arrays';
22
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
23
import { Schemas } from '../../../util/vs/base/common/network';
24
import { mixin } from '../../../util/vs/base/common/objects';
25
import { isEqual } from '../../../util/vs/base/common/resources';
26
import { URI } from '../../../util/vs/base/common/uri';
27
import { generateUuid } from '../../../util/vs/base/common/uuid';
28
import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';
29
import { ChatRequestEditorData, ChatRequestNotebookData, ChatRequestTurn, ChatResponseAnchorPart, ChatResponseFileTreePart, ChatResponseMarkdownPart, ChatResponseProgressPart2, ChatResponseReferencePart, ChatResponseTurn, ChatLocation as VSChatLocation } from '../../../vscodeTypes';
30
import { ICommandService } from '../../commands/node/commandService';
31
import { getAgentForIntent, Intent } from '../../common/constants';
32
import { IConversationStore } from '../../conversationStore/node/conversationStore';
33
import { IIntentService } from '../../intents/node/intentService';
34
import { UnknownIntent } from '../../intents/node/unknownIntent';
35
import { ContributedToolName } from '../../tools/common/toolNames';
36
import { ChatVariablesCollection } from '../common/chatVariablesCollection';
37
import { AnthropicTokenUsageMetadata, Conversation, getGlobalContextCacheKey, GlobalContextMessageMetadata, ICopilotChatResult, ICopilotChatResultIn, normalizeSummariesOnRounds, RenderedUserMessageMetadata, Turn, TurnStatus } from '../common/conversation';
38
import { InternalToolReference } from '../common/intents';
39
import { ChatTelemetryBuilder } from './chatParticipantTelemetry';
40
import { DefaultIntentRequestHandler } from './defaultIntentRequestHandler';
41
import { IDocumentContext } from './documentContext';
42
import { IntentDetector } from './intentDetector';
43
import { CommandDetails } from './intentRegistry';
44
import { IIntent } from './intents';
45
46
export interface IChatAgentArgs {
47
agentName: string;
48
agentId: string;
49
intentId?: string;
50
}
51
52
/**
53
* Handles a single chat request:
54
* 1) selects intent
55
* 2) invoke intent via `IIntentRequestHandler/AbstractIntentRequestHandler`
56
*/
57
export class ChatParticipantRequestHandler {
58
59
public readonly conversation: Conversation;
60
61
private readonly location: ChatLocation;
62
private readonly stream: ChatResponseStream;
63
private readonly documentContext: IDocumentContext | undefined;
64
private readonly intentDetector: IntentDetector;
65
private readonly turn: Turn;
66
67
private readonly chatTelemetry: ChatTelemetryBuilder;
68
69
constructor(
70
private readonly rawHistory: ReadonlyArray<ChatRequestTurn | ChatResponseTurn>,
71
private request: ChatRequest,
72
stream: ChatResponseStream,
73
private readonly token: CancellationToken,
74
private readonly chatAgentArgs: IChatAgentArgs,
75
private readonly yieldRequested: () => boolean,
76
telemetryMessageId: string | undefined,
77
@IInstantiationService private readonly _instantiationService: IInstantiationService,
78
@IEndpointProvider private readonly _endpointProvider: IEndpointProvider,
79
@ICommandService private readonly _commandService: ICommandService,
80
@IIgnoreService private readonly _ignoreService: IIgnoreService,
81
@IIntentService private readonly _intentService: IIntentService,
82
@IConversationStore private readonly _conversationStore: IConversationStore,
83
@ITabsAndEditorsService tabsAndEditorsService: ITabsAndEditorsService,
84
@ILogService private readonly _logService: ILogService,
85
@IAuthenticationService private readonly _authService: IAuthenticationService,
86
@IAuthenticationChatUpgradeService private readonly _authenticationUpgradeService: IAuthenticationChatUpgradeService,
87
) {
88
this.location = this.getLocation(request);
89
90
this.intentDetector = this._instantiationService.createInstance(IntentDetector);
91
92
this.stream = stream;
93
94
if (request.location2 instanceof ChatRequestEditorData) {
95
96
// don't send back references that are the same as the document as the one from which
97
// the request has been made
98
99
const documentUri = request.location2.document.uri;
100
101
this.stream = ChatResponseStreamImpl.filter(stream, part => {
102
if (part instanceof ChatResponseReferencePart || part instanceof ChatResponseProgressPart2) {
103
const uri = URI.isUri(part.value) ? part.value : (<Location>part.value).uri;
104
return !isEqual(uri, documentUri);
105
}
106
return true;
107
});
108
}
109
110
const { turns, sessionId } = _instantiationService.invokeFunction(accessor => addHistoryToConversation(accessor, rawHistory));
111
normalizeSummariesOnRounds(turns);
112
// Use session ID from history, then VS Code's request.sessionId, then fallback to UUID
113
const actualSessionId = sessionId ?? request.sessionId ?? generateUuid();
114
115
this.documentContext = IDocumentContext.inferDocumentContext(request, tabsAndEditorsService.activeTextEditor, turns);
116
117
this.chatTelemetry = this._instantiationService.createInstance(ChatTelemetryBuilder,
118
Date.now(),
119
actualSessionId,
120
this.documentContext,
121
turns.length === 0,
122
this.request,
123
telemetryMessageId
124
);
125
126
const latestTurn = Turn.fromRequest(
127
this.chatTelemetry.telemetryMessageId,
128
this.request);
129
130
this.conversation = new Conversation(actualSessionId, turns.concat(latestTurn));
131
132
this.turn = latestTurn;
133
}
134
135
private getLocation(request: ChatRequest) {
136
if (request.location2 instanceof ChatRequestEditorData) {
137
return ChatLocation.Editor;
138
} else if (request.location2 instanceof ChatRequestNotebookData) {
139
return ChatLocation.Notebook;
140
}
141
switch (request.location) { // deprecated, but location2 does not yet allow to distinguish between panel, editing session and others
142
case VSChatLocation.Editor:
143
return ChatLocation.Editor;
144
case VSChatLocation.Panel:
145
return ChatLocation.Panel;
146
case VSChatLocation.Terminal:
147
return ChatLocation.Terminal;
148
default:
149
return ChatLocation.Other;
150
}
151
}
152
153
private async sanitizeVariables(): Promise<ChatRequest> {
154
const variablePromises = this.request.references.map(async (ref) => {
155
const uri = isLocation(ref.value) ? ref.value.uri : URI.isUri(ref.value) ? ref.value : undefined;
156
if (!uri) {
157
return ref;
158
}
159
160
if (uri.scheme === Schemas.untitled) {
161
return ref;
162
}
163
164
let removeVariable;
165
try {
166
// Filter out variables which contain paths which are ignored
167
removeVariable = await this._ignoreService.isCopilotIgnored(uri);
168
} catch {
169
// Non-existent files will be handled elsewhere. This might be a virtual document so it's ok if the fs service can't find it.
170
}
171
172
if (removeVariable && ref.range) {
173
// Also sanitize the user message since file paths are sensitive
174
this.turn.request.message = this.turn.request.message.slice(0, ref.range[0]) + this.turn.request.message.slice(ref.range[1]);
175
}
176
177
return removeVariable ? null : ref;
178
});
179
180
const newVariables = coalesce(await Promise.all(variablePromises));
181
182
return { ...this.request, references: newVariables };
183
}
184
185
private async _shouldAskForPermissiveAuth(): Promise<boolean> {
186
// The user has confirmed that they want to auth, so prompt them.
187
const findConfirmRequest = this.request.acceptedConfirmationData?.find(ref => ref?.authPermissionPrompted);
188
if (findConfirmRequest) {
189
this.request = await this._authenticationUpgradeService.handleConfirmationRequest(this.stream, this.request, this.rawHistory);
190
this.turn.request.message = this.request.prompt;
191
return false;
192
}
193
194
// Only ask for confirmation if we're invoking the codebase tool or workspace chat participant
195
const isWorkspaceCall = this.request.toolReferences.some(ref => ref.name === ContributedToolName.Codebase);
196
// and only if we can't access all repos in the workspace
197
if (isWorkspaceCall && await this._authenticationUpgradeService.shouldRequestPermissiveSessionUpgrade()) {
198
this._authenticationUpgradeService.showPermissiveSessionUpgradeInChat(this.stream, this.request);
199
return true;
200
}
201
return false;
202
}
203
204
async getResult(): Promise<ICopilotChatResult> {
205
if (await this._shouldAskForPermissiveAuth()) {
206
// Return a random response
207
return {
208
metadata: {
209
modelMessageId: this.turn.responseId ?? '',
210
responseId: this.turn.id,
211
sessionId: this.conversation.sessionId,
212
agentId: this.chatAgentArgs.agentId,
213
command: this.request.command,
214
}
215
};
216
}
217
this._logService.trace(`[${ChatLocation.toStringShorter(this.location)}] chat request received from extension host`);
218
try {
219
220
// sanitize the variables of all requests
221
// this is done here because all intents must honor ignored files
222
this.request = await this.sanitizeVariables();
223
224
const command = this.chatAgentArgs.intentId ?
225
this._commandService.getCommand(this.chatAgentArgs.intentId, this.location) :
226
undefined;
227
228
let result = this.checkCommandUsage(command);
229
230
if (!result) {
231
// this is norm-case, e.g checkCommandUsage didn't produce an error-result
232
// and we proceed with the actual intent invocation
233
234
const history = this.conversation.turns.slice(0, -1);
235
const intent = await this.selectIntent(command, history);
236
237
let chatResult: Promise<ChatResult>;
238
if (typeof intent.handleRequest === 'function') {
239
chatResult = intent.handleRequest(this.conversation, this.request, this.stream, this.token, this.documentContext, this.chatAgentArgs.agentName, this.location, this.chatTelemetry, this.yieldRequested);
240
} else {
241
const intentHandler = this._instantiationService.createInstance(DefaultIntentRequestHandler, intent, this.conversation, this.request, this.stream, this.token, this.documentContext, this.location, this.chatTelemetry, undefined, this.yieldRequested);
242
chatResult = intentHandler.getResult();
243
}
244
245
if (!this.request.isParticipantDetected) {
246
this.intentDetector.collectIntentDetectionContextInternal(
247
this.turn.request.message,
248
this.request.enableCommandDetection ? intent.id : 'none',
249
new ChatVariablesCollection(this.request.references),
250
this.location,
251
history,
252
this.documentContext?.document
253
);
254
}
255
256
result = await chatResult;
257
const endpoint = await this._endpointProvider.getChatEndpoint(this.request);
258
result.details = this._authService.copilotToken?.isNoAuthUser || endpoint.multiplier === undefined ?
259
`${endpoint.name}` :
260
`${endpoint.name} • ${endpoint.multiplier}x`;
261
}
262
263
this._conversationStore.addConversation(this.turn.id, this.conversation);
264
265
// mixin fixed metadata shape into result. Modified in place because the object is already
266
// cached in the conversation store and we want the full information when looking this up
267
// later
268
mixin(result, {
269
metadata: {
270
modelMessageId: this.turn.responseId ?? '',
271
responseId: this.turn.id,
272
sessionId: this.conversation.sessionId,
273
agentId: this.chatAgentArgs.agentId,
274
command: this.request.command
275
}
276
} satisfies ICopilotChatResult, true);
277
278
return <ICopilotChatResult>result;
279
280
} catch (err) {
281
// TODO This method should not throw at all, but return a result with errorDetails, and call the IConversationStore
282
throw err;
283
}
284
}
285
286
private async selectIntent(command: CommandDetails | undefined, history: Turn[]): Promise<IIntent> {
287
if (!command?.intent && this.location === ChatLocation.Editor) { // TODO@jrieken do away with location specific code
288
289
let preferredIntent: Intent | undefined;
290
if (this.documentContext && this.request.attempt === 0 && history.length === 0) {
291
if (this.documentContext.selection.isEmpty && this.documentContext.document.lineAt(this.documentContext.selection.start.line).text.trim() === '') {
292
preferredIntent = Intent.Generate;
293
} else if (!this.documentContext.selection.isEmpty && this.documentContext.selection.start.line !== this.documentContext.selection.end.line) {
294
preferredIntent = Intent.Edit;
295
}
296
}
297
if (preferredIntent) {
298
return this._intentService.getIntent(preferredIntent, this.location) ?? this._intentService.unknownIntent;
299
}
300
}
301
302
return command?.intent ?? this._intentService.unknownIntent;
303
}
304
305
private checkCommandUsage(command: CommandDetails | undefined): ChatResult | undefined {
306
if (command?.intent && !(command.intent.commandInfo?.allowsEmptyArgs ?? true) && !this.turn.request.message) {
307
const commandAgent = getAgentForIntent(command.intent.id as Intent, this.location);
308
let usage = '';
309
if (commandAgent) {
310
// If the command was used, it must have an agent
311
usage = `@${commandAgent.agent} `;
312
if (commandAgent.command) {
313
usage += ` /${commandAgent.command}`;
314
}
315
usage += ` ${command.details}`;
316
317
}
318
319
const message = l10n.t(`Please specify a question when using this command.\n\nUsage: {0}`, usage);
320
const chatResult = { errorDetails: { message } };
321
this.turn.setResponse(TurnStatus.Error, { type: 'meta', message }, undefined, chatResult);
322
return chatResult;
323
}
324
}
325
}
326
327
328
export function addHistoryToConversation(accessor: ServicesAccessor, history: ReadonlyArray<ChatRequestTurn | ChatResponseTurn>): { turns: Turn[]; sessionId: string | undefined } {
329
const instaService = accessor.get(IInstantiationService);
330
331
const turns: Turn[] = [];
332
let sessionId: string | undefined;
333
let previousChatRequestTurn: ChatRequestTurn | undefined;
334
335
for (const entry of history) {
336
// The extension API model technically supports arbitrary requests/responses not in pairs, but this isn't used anywhere,
337
// so we can just fit this to our Conversation model for now.
338
if (entry instanceof ChatRequestTurn) {
339
previousChatRequestTurn = entry;
340
} else {
341
const existingTurn = instaService.invokeFunction(findExistingTurnFromVSCodeChatHistoryTurn, entry);
342
if (existingTurn) {
343
turns.push(existingTurn);
344
} else {
345
if (previousChatRequestTurn) {
346
const deserializedTurn = instaService.invokeFunction(createTurnFromVSCodeChatHistoryTurns, previousChatRequestTurn, entry);
347
previousChatRequestTurn = undefined;
348
turns.push(deserializedTurn);
349
}
350
}
351
352
const copilotResult = entry.result as ICopilotChatResultIn;
353
if (typeof copilotResult.metadata?.sessionId === 'string') {
354
sessionId = copilotResult.metadata.sessionId;
355
}
356
}
357
}
358
359
return { turns, sessionId };
360
}
361
362
/**
363
* Try to find an existing `Turn` instance that we created previously based on the responseId of a vscode turn.
364
*/
365
function findExistingTurnFromVSCodeChatHistoryTurn(accessor: ServicesAccessor, turn: ChatRequestTurn | ChatResponseTurn): Turn | undefined {
366
const conversationStore = accessor.get(IConversationStore);
367
const responseId = getResponseIdFromVSCodeChatHistoryTurn(turn);
368
const conversation = responseId ? conversationStore.getConversation(responseId) : undefined;
369
return conversation?.turns.find(turn => turn.id === responseId);
370
}
371
372
function getResponseIdFromVSCodeChatHistoryTurn(turn: ChatRequestTurn | ChatResponseTurn): string | undefined {
373
if (turn instanceof ChatResponseTurn) {
374
const lastEntryResult = turn.result as ICopilotChatResultIn | undefined;
375
return lastEntryResult?.metadata?.responseId;
376
}
377
return undefined;
378
}
379
380
/**
381
* Try as best as possible to create a `Turn` object from data that comes from vscode.
382
*/
383
function createTurnFromVSCodeChatHistoryTurns(
384
accessor: ServicesAccessor,
385
chatRequestTurn: ChatRequestTurn,
386
chatResponseTurn: ChatResponseTurn
387
): Turn {
388
const commandService = accessor.get(ICommandService);
389
const workspaceService = accessor.get(IWorkspaceService);
390
const instaService = accessor.get(IInstantiationService);
391
392
const chatRequestAsTurn2 = chatRequestTurn as ChatRequestTurn2;
393
const currentTurn = new Turn(
394
undefined,
395
{ message: chatRequestTurn.prompt, type: 'user' },
396
new ChatVariablesCollection(chatRequestTurn.references),
397
chatRequestTurn.toolReferences.map(InternalToolReference.from),
398
chatRequestAsTurn2.editedFileEvents,
399
undefined,
400
false,
401
chatRequestAsTurn2.modeInstructions2,
402
);
403
404
// Take just the content messages
405
const content = chatResponseTurn.response.map(r => {
406
if (r instanceof ChatResponseMarkdownPart) {
407
return r.value.value;
408
} else if (r instanceof ChatResponseFileTreePart) {
409
return fileTreePartToMarkdown(r);
410
} else if ('content' in r) {
411
return r.content;
412
} else if (r instanceof ChatResponseAnchorPart) {
413
return anchorPartToMarkdown(workspaceService, r);
414
} else {
415
return null;
416
}
417
}).filter(Boolean).join('');
418
const intentId = chatResponseTurn.command || getChatParticipantNameFromId(chatResponseTurn.participant);
419
const command = commandService.getCommand(intentId, ChatLocation.Panel);
420
let status: TurnStatus;
421
if (!chatResponseTurn.result.errorDetails) {
422
status = TurnStatus.Success;
423
} else if (chatResponseTurn.result.errorDetails?.responseIsFiltered) {
424
if (chatResponseTurn.result.metadata?.category === FilterReason.Prompt) {
425
status = TurnStatus.PromptFiltered;
426
} else {
427
status = TurnStatus.Filtered;
428
}
429
} else if (chatResponseTurn.result.errorDetails.message === 'Cancelled' || chatResponseTurn.result.errorDetails.message === CanceledMessage.message) {
430
status = TurnStatus.Cancelled;
431
} else {
432
status = TurnStatus.Error;
433
}
434
435
currentTurn.setResponse(status, { message: content, type: 'model', name: command?.commandId || UnknownIntent.ID }, undefined, chatResponseTurn.result);
436
const turnMetadata = (chatResponseTurn.result as ICopilotChatResultIn).metadata;
437
if (turnMetadata?.renderedGlobalContext) {
438
const cacheKey = turnMetadata.globalContextCacheKey ?? instaService.invokeFunction(getGlobalContextCacheKey);
439
currentTurn.setMetadata(new GlobalContextMessageMetadata(turnMetadata?.renderedGlobalContext, cacheKey));
440
}
441
if (turnMetadata?.renderedUserMessage) {
442
currentTurn.setMetadata(new RenderedUserMessageMetadata(turnMetadata.renderedUserMessage));
443
}
444
if (turnMetadata?.promptTokens && turnMetadata?.outputTokens) {
445
currentTurn.setMetadata(new AnthropicTokenUsageMetadata(turnMetadata.promptTokens, turnMetadata.outputTokens));
446
}
447
448
return currentTurn;
449
}
450
451
function anchorPartToMarkdown(workspaceService: IWorkspaceService, anchor: ChatResponseAnchorPart): string {
452
let text: string;
453
let path: string;
454
455
if (URI.isUri(anchor.value)) {
456
path = getWorkspaceFileDisplayPath(workspaceService, anchor.value);
457
const label = anchor.title ?? path;
458
text = `\`${label}\``;
459
} else if (isLocation(anchor.value)) {
460
path = getWorkspaceFileDisplayPath(workspaceService, anchor.value.uri);
461
const label = anchor.title ?? `${path}#L${anchor.value.range.start.line + 1}${anchor.value.range.start.line === anchor.value.range.end.line ? '' : `-${anchor.value.range.end.line + 1}`}`;
462
text = `\`${label}\``;
463
} else if (isSymbolInformation(anchor.value)) {
464
path = getWorkspaceFileDisplayPath(workspaceService, anchor.value.location.uri);
465
text = `\`${anchor.value.name}\``;
466
} else {
467
// Unknown anchor type
468
return '';
469
}
470
471
return `[${text}](${path} ${anchor.title ? `"${anchor.title}"` : ''})`;
472
}
473
474