Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts
13405 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 { Raw } from '@vscode/prompt-tsx';
8
import type { ChatErrorDetails, MappedEditsResponseStream, NotebookCell, NotebookDocument, Uri } from 'vscode';
9
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
10
import { FetchStreamSource, IResponsePart } from '../../../../platform/chat/common/chatMLFetcher';
11
import { ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError, getFilteredMessage } from '../../../../platform/chat/common/commonTypes';
12
import { getTextPart, toTextPart } from '../../../../platform/chat/common/globalStringUtils';
13
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
14
import { IDiffService } from '../../../../platform/diff/common/diffService';
15
import { NotebookDocumentSnapshot } from '../../../../platform/editing/common/notebookDocumentSnapshot';
16
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
17
import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';
18
import { ChatEndpoint } from '../../../../platform/endpoint/node/chatEndpoint';
19
import { Proxy4oEndpoint } from '../../../../platform/endpoint/node/proxy4oEndpoint';
20
import { ProxyInstantApplyShortEndpoint } from '../../../../platform/endpoint/node/proxyInstantApplyShortEndpoint';
21
import { IOctoKitService } from '../../../../platform/github/common/githubService';
22
import { ILogService } from '../../../../platform/log/common/logService';
23
import { IEditLogService } from '../../../../platform/multiFileEdit/common/editLogService';
24
import { IMultiFileEditInternalTelemetryService } from '../../../../platform/multiFileEdit/common/multiFileEditQualityTelemetry';
25
import { Completion } from '../../../../platform/nesFetch/common/completionsAPI';
26
import { CompletionsFetchError } from '../../../../platform/nesFetch/common/completionsFetchService';
27
import { FinishedCallback, IResponseDelta } from '../../../../platform/networking/common/fetch';
28
import { FilterReason } from '../../../../platform/networking/common/openai';
29
import { IAlternativeNotebookContentEditGenerator, NotebookEditGenerationTelemtryOptions, NotebookEditGenrationSource } from '../../../../platform/notebook/common/alternativeContentEditGenerator';
30
import { INotebookService } from '../../../../platform/notebook/common/notebookService';
31
import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
32
import { NullTelemetryService } from '../../../../platform/telemetry/common/nullTelemetryService';
33
import { ITelemetryService, multiplexProperties } from '../../../../platform/telemetry/common/telemetry';
34
import { ITokenizerProvider } from '../../../../platform/tokenizer/node/tokenizer';
35
import { getLanguageForResource } from '../../../../util/common/languages';
36
import { getFenceForCodeBlock, languageIdToMDCodeBlockLang } from '../../../../util/common/markdown';
37
import { ITokenizer } from '../../../../util/common/tokenizer';
38
import { equals } from '../../../../util/vs/base/common/arrays';
39
import { assertNever } from '../../../../util/vs/base/common/assert';
40
import { AsyncIterableObject } from '../../../../util/vs/base/common/async';
41
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
42
import { ResourceMap } from '../../../../util/vs/base/common/map';
43
import { isEqual } from '../../../../util/vs/base/common/resources';
44
import { URI } from '../../../../util/vs/base/common/uri';
45
import { generateUuid } from '../../../../util/vs/base/common/uuid';
46
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
47
import { NotebookEdit, Position, Range, TextEdit } from '../../../../vscodeTypes';
48
import { OutcomeAnnotation, OutcomeAnnotationLabel } from '../../../inlineChat/node/promptCraftingTypes';
49
import { Lines, LinesEdit } from '../../../prompt/node/editGeneration';
50
import { LineOfText, PartialAsyncTextReader } from '../../../prompt/node/streamingEdits';
51
import { PromptRenderer } from '../../../prompts/node/base/promptRenderer';
52
import { EXISTING_CODE_MARKER } from '../panel/codeBlockFormattingRules';
53
import { CodeMapperFullRewritePrompt, CodeMapperPatchRewritePrompt, CodeMapperPromptProps } from './codeMapperPrompt';
54
import { ICodeMapperTelemetryInfo } from './codeMapperService';
55
import { findEdit, getCodeBlock, iterateSectionsForResponse, Marker, Patch, Section } from './patchEditGeneration';
56
57
58
export type ICodeMapperDocument = TextDocumentSnapshot | NotebookDocumentSnapshot;
59
60
export async function processFullRewriteNotebook(document: NotebookDocument, inputStream: string | AsyncIterable<LineOfText>, outputStream: MappedEditsResponseStream, alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, telemetryOptions: NotebookEditGenerationTelemtryOptions, token: CancellationToken): Promise<void> {
61
for await (const edit of processFullRewriteNotebookEdits(document, inputStream, alternativeNotebookEditGenerator, telemetryOptions, token)) {
62
if (Array.isArray(edit)) {
63
outputStream.textEdit(edit[0], edit[1]);
64
} else {
65
outputStream.notebookEdit(document.uri, edit); // changed this.outputStream to outputStream
66
}
67
}
68
69
return undefined;
70
}
71
72
export type CellOrNotebookEdit = NotebookEdit | [Uri, TextEdit[]];
73
74
export async function* processFullRewriteNotebookEdits(document: NotebookDocument, inputStream: string | AsyncIterable<LineOfText>, alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, telemetryOptions: NotebookEditGenerationTelemtryOptions, token: CancellationToken): AsyncIterable<CellOrNotebookEdit> {
75
// emit start of notebook
76
const cellMap = new ResourceMap<NotebookCell>();
77
for await (const edit of alternativeNotebookEditGenerator.generateNotebookEdits(document, inputStream, telemetryOptions, token)) {
78
if (Array.isArray(edit)) {
79
const cellUri = edit[0];
80
const cell = cellMap.get(cellUri) || document.getCells().find(cell => isEqual(cell.document.uri, cellUri));
81
if (cell) {
82
cellMap.set(cellUri, cell);
83
if (edit[1].length === 1 && edit[1][0].range.isSingleLine && cell.document.lineCount > edit[1][0].range.start.line) {
84
if (cell.document.lineAt(edit[1][0].range.start.line).text === edit[1][0].newText) {
85
continue;
86
}
87
}
88
yield [cellUri, edit[1]];
89
}
90
} else {
91
yield edit;
92
}
93
}
94
95
return undefined;
96
}
97
98
export async function processFullRewriteNewNotebook(uri: URI, source: string, outputStream: MappedEditsResponseStream, alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, telemetryOptions: NotebookEditGenerationTelemtryOptions, token: CancellationToken): Promise<void> {
99
for await (const edit of alternativeNotebookEditGenerator.generateNotebookEdits(uri, source, telemetryOptions, token)) {
100
if (!Array.isArray(edit)) {
101
outputStream.notebookEdit(uri, edit);
102
}
103
}
104
105
return undefined;
106
}
107
108
function emitCodeLine(line: string, uri: Uri, existingDocument: TextDocumentSnapshot | undefined, outputStream: MappedEditsResponseStream, pushedLines: string[], token: CancellationToken) {
109
if (token.isCancellationRequested) {
110
return undefined;
111
}
112
113
const lineCount = existingDocument ? existingDocument.lineCount : 0;
114
const currentLineIndex = pushedLines.length;
115
pushedLines.push(line);
116
117
if (currentLineIndex < lineCount) {
118
// this line exists in the doc => replace it
119
const currentLineLength = existingDocument ? existingDocument.lineAt(currentLineIndex).text.length : 0;
120
outputStream.textEdit(uri, [TextEdit.replace(new Range(currentLineIndex, 0, currentLineIndex, currentLineLength), line)]);
121
} else {
122
// we are at the end of the document
123
const addedText = currentLineIndex === 0 ? line : `\n` + line;
124
outputStream.textEdit(uri, [TextEdit.replace(new Range(currentLineIndex, 0, currentLineIndex, 0), addedText)]);
125
}
126
}
127
128
async function processFullRewriteStream(uri: Uri, existingDocument: TextDocumentSnapshot | undefined, inputStream: AsyncIterable<LineOfText>, outputStream: MappedEditsResponseStream, token: CancellationToken, pushedLines: string[] = []) {
129
for await (const line of inputStream) {
130
emitCodeLine(line.value, uri, existingDocument, outputStream, pushedLines, token);
131
}
132
133
return pushedLines;
134
}
135
136
async function handleTrailingLines(uri: Uri, existingDocument: TextDocumentSnapshot | undefined, outputStream: MappedEditsResponseStream, pushedLines: string[], token: CancellationToken): Promise<void> {
137
const lineCount = existingDocument ? existingDocument.lineCount : 0;
138
const initialTrailingEmptyLineCount = existingDocument ? getTrailingDocumentEmptyLineCount(existingDocument) : 0;
139
140
// The LLM does not want to produce trailing newlines
141
// Here we try to maintain the exact same tralining newlines count as the original document had
142
const pushedTrailingEmptyLineCount = getTrailingArrayEmptyLineCount(pushedLines);
143
for (let i = pushedTrailingEmptyLineCount; i < initialTrailingEmptyLineCount; i++) {
144
emitCodeLine('', uri, existingDocument, outputStream, pushedLines, token);
145
}
146
147
// Make sure we delete everything after the changed lines
148
const currentLineIndex = pushedLines.length;
149
if (currentLineIndex < lineCount) {
150
const from = currentLineIndex === 0 ? new Position(0, 0) : new Position(currentLineIndex - 1, pushedLines[pushedLines.length - 1].length);
151
outputStream.textEdit(uri, [TextEdit.delete(new Range(from, new Position(lineCount, 0)))]);
152
}
153
}
154
155
async function processFullRewriteResponseCode(uri: Uri, existingDocument: TextDocumentSnapshot | undefined, inputStream: AsyncIterable<LineOfText>, outputStream: MappedEditsResponseStream, token: CancellationToken): Promise<void> {
156
const pushedLines = await processFullRewriteStream(uri, existingDocument, inputStream, outputStream, token);
157
158
if (token.isCancellationRequested) {
159
return;
160
}
161
162
await handleTrailingLines(uri, existingDocument, outputStream, pushedLines, token);
163
}
164
165
/**
166
* Extract a fenced code block from a reply and emit the lines in the code block one-by-one.
167
*/
168
function extractCodeBlock(inputStream: AsyncIterable<IResponsePart>, token: CancellationToken): AsyncIterable<LineOfText> {
169
return new AsyncIterableObject<LineOfText>(async (emitter) => {
170
const fence = '```';
171
const textStream = AsyncIterableObject.map(inputStream, part => part.delta.text);
172
const reader = new PartialAsyncTextReader(textStream[Symbol.asyncIterator]());
173
174
let inCodeBlock = false;
175
while (!reader.endOfStream) {
176
// Skip everything until we hit a fence
177
if (token.isCancellationRequested) {
178
break;
179
}
180
const line = await reader.readLine();
181
if (line.startsWith(fence) && inCodeBlock) {
182
// Done reading code block, stop reading
183
inCodeBlock = false;
184
break;
185
} else if (line.startsWith(fence)) {
186
inCodeBlock = true;
187
} else if (inCodeBlock) {
188
emitter.emitOne(new LineOfText(line));
189
}
190
}
191
});
192
}
193
194
export async function processPatchResponse(uri: URI, originalText: string | undefined, inputStream: AsyncIterable<IResponsePart>, outputStream: MappedEditsResponseStream, token: CancellationToken): Promise<void> {
195
let documentLines = originalText ? Lines.fromString(originalText) : [];
196
function processAndEmitPatch(patch: Patch) {
197
// Make sure it's valid, otherwise emit
198
if (equals(patch.find, patch.replace)) {
199
return;
200
}
201
const res = findEdit(documentLines, getCodeBlock(patch.find), getCodeBlock(patch.replace), 0);
202
203
if (res instanceof LinesEdit) {
204
outputStream.textEdit(uri, res.toTextEdit());
205
documentLines = res.apply(documentLines);
206
}
207
}
208
209
let original, filePath;
210
const otherSections: Section[] = [];
211
for await (const section of iterateSectionsForResponse(inputStream)) {
212
switch (section.marker) {
213
case undefined:
214
break;
215
case Marker.FILEPATH:
216
filePath = section.content.join('\n').trim();
217
break;
218
case Marker.FIND:
219
original = section.content;
220
break;
221
case Marker.REPLACE: {
222
if (section.content && original && filePath) {
223
processAndEmitPatch({ filePath, find: original, replace: section.content });
224
}
225
break;
226
}
227
case Marker.COMPLETE:
228
break;
229
default:
230
otherSections.push(section);
231
break;
232
}
233
}
234
}
235
236
export interface ICodeMapperNewDocument {
237
readonly createNew: true;
238
readonly codeBlock: string;
239
readonly markdownBeforeBlock: string | undefined;
240
readonly uri: Uri;
241
readonly existingDocument: ICodeMapperDocument | undefined;
242
readonly workingSet: ICodeMapperDocument[];
243
}
244
245
export interface ICodeMapperExistingDocument {
246
readonly createNew: false;
247
readonly codeBlock: string;
248
readonly markdownBeforeBlock: string | undefined;
249
readonly uri: Uri;
250
readonly existingDocument: ICodeMapperDocument;
251
readonly location?: string;
252
}
253
254
export type ICodeMapperRequestInput = ICodeMapperNewDocument | ICodeMapperExistingDocument;
255
256
export function isNewDocument(input: ICodeMapperRequestInput): input is ICodeMapperNewDocument {
257
return input.createNew;
258
}
259
260
interface IFullRewritePrompt {
261
readonly prompt: string;
262
readonly messages: Raw.ChatMessage[];
263
264
readonly requestId: string;
265
266
readonly languageId: string;
267
268
readonly speculation: string;
269
readonly stopTokens: string[];
270
271
readonly promptTokenCount: number;
272
readonly speculationTokenCount: number;
273
274
readonly endpoint: ChatEndpoint;
275
readonly tokenizer: ITokenizer;
276
}
277
278
interface ICompletedRequest {
279
readonly startTime: number;
280
readonly firstTokenTime: number;
281
readonly responseText: string;
282
readonly requestId: string;
283
}
284
285
export class CodeMapper {
286
287
static closingXmlTag = 'copilot-edited-file';
288
private shortContextLimit: number;
289
290
constructor(
291
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
292
@IInstantiationService private readonly instantiationService: IInstantiationService,
293
@ITokenizerProvider private readonly tokenizerProvider: ITokenizerProvider,
294
@ILogService private readonly logService: ILogService,
295
@ITelemetryService private readonly telemetryService: ITelemetryService,
296
@IEditLogService private readonly editLogService: IEditLogService,
297
@IExperimentationService private readonly experimentationService: IExperimentationService,
298
@IDiffService private readonly diffService: IDiffService,
299
@IMultiFileEditInternalTelemetryService private readonly multiFileEditInternalTelemetryService: IMultiFileEditInternalTelemetryService,
300
@IAlternativeNotebookContentEditGenerator private readonly alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator,
301
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
302
@IOctoKitService private readonly octoKitService: IOctoKitService,
303
@INotebookService private readonly notebookService: INotebookService,
304
@IConfigurationService configurationService: IConfigurationService,
305
) {
306
this.shortContextLimit = configurationService.getExperimentBasedConfig<number>(ConfigKey.Advanced.InstantApplyShortContextLimit, experimentationService) ?? 8000;
307
}
308
309
private async getGpt4oProxyEndpoint(): Promise<Proxy4oEndpoint> {
310
await this.experimentationService.hasTreatments();
311
return this.instantiationService.createInstance(Proxy4oEndpoint);
312
}
313
314
private async getShortIAEndpoint(): Promise<ProxyInstantApplyShortEndpoint> {
315
await this.experimentationService.hasTreatments();
316
return this.instantiationService.createInstance(ProxyInstantApplyShortEndpoint);
317
}
318
319
public async mapCode(request: ICodeMapperRequestInput, resultStream: MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: CancellationToken): Promise<CodeMapperOutcome | undefined> {
320
321
const fastEdit = await this.mapCodeUsingFastEdit(request, resultStream, telemetryInfo, token);
322
if (!(fastEdit instanceof CodeMapperRefusal)) {
323
return fastEdit;
324
}
325
// continue with "slow rewrite endpoint" when fast rewriting was not possible
326
// use copilot base as fallback
327
const chatEndpoint = await this.endpointProvider.getChatEndpoint('copilot-base');
328
329
// Only attempt a full file rewrite if the original document fits into 3/4 of the max output token limit, leaving space for the model to add code. The limit is currently a flat 4K tokens from CAPI across all our models.
330
// If there are multiple input documents, pick the longest one to base the limit on
331
const longestDocumentContext = isNewDocument(request) ? request.workingSet.reduce<ICodeMapperDocument | undefined>((prev, curr) => (prev && (prev.getText().length > curr.getText().length)) ? prev : curr, undefined) : request.existingDocument;
332
const doFullRewrite = longestDocumentContext ? await chatEndpoint.acquireTokenizer().tokenLength(longestDocumentContext.getText()) < (4096 / 4 * 3) : true;
333
334
const existingDocument = request.existingDocument;
335
336
const fetchStreamSource = new FetchStreamSource();
337
338
const cb: FinishedCallback = async (text: string, index: number, delta: IResponseDelta) => {
339
fetchStreamSource.update(text, delta);
340
return undefined;
341
};
342
let responsePromise: Promise<void> | undefined;
343
if (doFullRewrite) {
344
if (existingDocument && existingDocument instanceof NotebookDocumentSnapshot) { // TODO@joyceerhl: Handle notebook document response processing
345
const telemtryOptions: NotebookEditGenerationTelemtryOptions = {
346
source: NotebookEditGenrationSource.codeMapperEditNotebook,
347
requestId: undefined,
348
model: chatEndpoint.model
349
};
350
responsePromise = processFullRewriteNotebook(existingDocument.document, extractCodeBlock(fetchStreamSource.stream, token), resultStream, this.alternativeNotebookEditGenerator, telemtryOptions, token);
351
} else {
352
responsePromise = processFullRewriteResponseCode(request.uri, existingDocument, extractCodeBlock(fetchStreamSource.stream, token), resultStream, token);
353
}
354
} else {
355
responsePromise = processPatchResponse(request.uri, existingDocument?.getText(), fetchStreamSource.stream, resultStream, token);
356
}
357
358
const promptRenderer = PromptRenderer.create(
359
this.instantiationService,
360
chatEndpoint,
361
doFullRewrite ? CodeMapperFullRewritePrompt : CodeMapperPatchRewritePrompt,
362
{ request } satisfies CodeMapperPromptProps
363
);
364
365
const prompt = await promptRenderer.render(undefined, token);
366
if (token.isCancellationRequested) {
367
return undefined;
368
}
369
const fetchResult = await chatEndpoint.makeChatRequest(
370
'codeMapper',
371
prompt.messages,
372
cb,
373
token,
374
ChatLocation.Other,
375
undefined,
376
{ temperature: 0 }
377
);
378
379
fetchStreamSource.resolve();
380
await responsePromise; // Make sure we push all text edits to the response stream
381
382
let result: CodeMapperOutcome;
383
384
const createOutcome = (annotations: OutcomeAnnotation[], errorDetails: ChatErrorDetails | undefined): CodeMapperOutcome => {
385
return ({ errorDetails, annotations, telemetry: { requestId: String(telemetryInfo?.chatRequestId), speculationRequestId: fetchResult.requestId, requestSource: String(telemetryInfo?.chatRequestSource), mapper: doFullRewrite ? 'full' : 'patch' } });
386
};
387
if (fetchResult.type === ChatFetchResponseType.Success) {
388
result = createOutcome([], undefined);
389
} else {
390
if (fetchResult.type === ChatFetchResponseType.Canceled) {
391
return undefined;
392
}
393
const outageStatus = await this.octoKitService.getGitHubOutageStatus();
394
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this.authenticationService.getCopilotToken()).copilotPlan, outageStatus);
395
result = createOutcome([{ label: errorDetails.message, message: `request ${fetchResult.type}`, severity: 'error' }], errorDetails);
396
}
397
if (result.annotations.length || result.errorDetails) {
398
this.logService.info(`[code mapper] Problems generating edits: ${result.annotations.map(a => `${a.message} [${a.label}]`).join(', ')}, ${result.errorDetails?.message}`);
399
}
400
return result;
401
}
402
403
//#region Full file rewrite with speculation / predicted outputs
404
405
private async buildPrompt(request: ICodeMapperRequestInput, token: CancellationToken): Promise<IFullRewritePrompt> {
406
let endpoint: ChatEndpoint = await this.getGpt4oProxyEndpoint();
407
const tokenizer = this.tokenizerProvider.acquireTokenizer(endpoint);
408
const requestId = generateUuid();
409
410
const promptRenderer = PromptRenderer.create(
411
this.instantiationService,
412
endpoint,
413
CodeMapperFullRewritePrompt,
414
{ request, shouldTrimCodeBlocks: true } satisfies CodeMapperPromptProps
415
);
416
const uri = request.uri;
417
418
const promptRendererResult = await promptRenderer.render(undefined, token);
419
const fence = isNewDocument(request) ? '```' : getFenceForCodeBlock(request.existingDocument.getText());
420
const languageId = isNewDocument(request) ? getLanguageForResource(uri).languageId : request.existingDocument.languageId;
421
const speculation = isNewDocument(request) ? '' : request.existingDocument.getText();
422
const messages: Raw.ChatMessage[] = [{
423
role: Raw.ChatRole.User,
424
content: [toTextPart(promptRendererResult.messages.reduce((prev, curr) => {
425
const content = getTextPart(curr.content);
426
if (curr.role === Raw.ChatRole.System) {
427
const currentContent = content.endsWith('\n') ? content : `${content}\n`;
428
return `${prev}<SYSTEM>\n${currentContent}</SYSTEM>\n\n\n`;
429
}
430
return prev + content;
431
}, ''))]
432
}];
433
const prompt = promptRendererResult.messages.reduce((prev, curr) => {
434
const content = getTextPart(curr.content);
435
if (curr.role === Raw.ChatRole.System) {
436
const currentContent = content.endsWith('\n') ? content : `${content}\n`;
437
return `${prev}<SYSTEM>\n${currentContent}\nEnd your response with </${CodeMapper.closingXmlTag}>.\n</SYSTEM>\n\n\n`;
438
}
439
return prev + content;
440
}, '').trimEnd() + `\n\n\nThe resulting document:\n<${CodeMapper.closingXmlTag}>\n${fence}${languageIdToMDCodeBlockLang(languageId)}\n`;
441
442
if (prompt.length < this.shortContextLimit) {
443
endpoint = await this.getShortIAEndpoint();
444
}
445
446
const promptTokenCount = await tokenizer.tokenLength(prompt);
447
const speculationTokenCount = await tokenizer.tokenLength(speculation);
448
const stopTokens = [`${fence}\n</${CodeMapper.closingXmlTag}>`, `${fence}\r\n</${CodeMapper.closingXmlTag}>`, `</${CodeMapper.closingXmlTag}>`];
449
450
return { prompt, requestId, messages, speculation, stopTokens, promptTokenCount, speculationTokenCount, endpoint, tokenizer, languageId };
451
}
452
453
private async logDoneInfo(request: ICodeMapperRequestInput, prompt: IFullRewritePrompt, response: ICompletedRequest, telemetryInfo: CodeMapperOutcomeTelemetry, mapper: string, annotations: OutcomeAnnotation[]) {
454
if (this.telemetryService instanceof NullTelemetryService) {
455
// noo need to make all the computation
456
return;
457
}
458
459
const { speculation, tokenizer, promptTokenCount, speculationTokenCount } = prompt;
460
const { firstTokenTime, startTime, responseText, requestId } = response;
461
462
const timeToFirstToken = firstTokenTime === -1 ? -1 : firstTokenTime - startTime;
463
const timeToComplete = Date.now() - startTime;
464
this.logService.info(`srequest done: ${timeToComplete}ms, chatRequestId: [${telemetryInfo?.requestId}], speculationRequestId: [${requestId}]`);
465
const isNoopEdit = responseText.trim() === speculation.trim();
466
467
const { addedLines, removedLines } = await computeAdditionsAndDeletions(this.diffService, speculation, responseText);
468
469
/* __GDPR__
470
"speculation.response.success" : {
471
"owner": "alexdima",
472
"comment": "Report quality details for a successful speculative response.",
473
"chatRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },
474
"chatRequestSource": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Source of the current turn request" },
475
"isNoopEdit": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the response text is identical to the speculation." },
476
"speculationRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },
477
"containsElidedCodeComments": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the response text contains elided code comments." },
478
"model": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The model used for this speculation request" },
479
"promptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens", "isMeasurement": true },
480
"speculationTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of speculation tokens", "isMeasurement": true },
481
"responseTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of response tokens", "isMeasurement": true },
482
"addedLines": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of lines added", "isMeasurement": true },
483
"removedLines": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of lines removed", "isMeasurement": true },
484
"isNotebook": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether this is a notebook", "isMeasurement": true },
485
"timeToFirstToken": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to first token", "isMeasurement": true },
486
"timeToComplete": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to complete the request", "isMeasurement": true }
487
}
488
*/
489
this.telemetryService.sendMSFTTelemetryEvent('speculation.response.success', {
490
chatRequestId: telemetryInfo?.requestId,
491
chatRequestSource: telemetryInfo?.requestSource,
492
speculationRequestId: requestId,
493
isNoopEdit: String(isNoopEdit),
494
containsElidedCodeComments: String(responseText.includes(EXISTING_CODE_MARKER)),
495
model: mapper
496
}, {
497
promptTokenCount,
498
speculationTokenCount,
499
responseTokenCount: await tokenizer.tokenLength(responseText),
500
timeToFirstToken,
501
timeToComplete,
502
addedLines,
503
removedLines,
504
isNotebook: this.notebookService.hasSupportedNotebooks(request.uri) ? 1 : 0
505
});
506
if (isNoopEdit) {
507
const message = 'Speculative response is identical to speculation, srequest: ' + requestId + ', URI: ' + request.uri.toString();
508
annotations.push({ label: OutcomeAnnotationLabel.NOOP_EDITS, message, severity: 'error' });
509
}
510
}
511
512
private async logError(request: ICodeMapperRequestInput, prompt: IFullRewritePrompt, response: Omit<ICompletedRequest, 'responseText'>, telemetryInfo: CodeMapperOutcomeTelemetry, mapper: string, errorMessage: string, error?: Error) {
513
const { promptTokenCount, speculationTokenCount } = prompt;
514
const { startTime, requestId } = response;
515
516
this.logService.error(`srequest failed: ${Date.now() - startTime}ms, chatRequestId: [${telemetryInfo?.requestId}], speculationRequestId: [${requestId}] error: [${errorMessage}]`);
517
if (error) {
518
this.logService.error(error);
519
}
520
/* __GDPR__
521
"speculation.response.error" : {
522
"owner": "alexdima",
523
"comment": "Report quality issue for when a speculative response failed.",
524
"errorMessage": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The name of the error" },
525
"chatRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },
526
"speculationRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the speculation request" },
527
"chatRequestSource": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Source of the current turn request" },
528
"model": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The model used for this speculation request" },
529
"promptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens", "isMeasurement": true },
530
"speculationTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of speculation tokens", "isMeasurement": true },
531
"isNotebook": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether this is a notebook", "isMeasurement": true }
532
}
533
*/
534
this.telemetryService.sendMSFTTelemetryEvent('speculation.response.error', {
535
errorMessage,
536
chatRequestId: telemetryInfo?.requestId,
537
chatRequestSource: telemetryInfo?.requestSource,
538
speculationRequestId: requestId,
539
model: mapper
540
}, {
541
promptTokenCount,
542
speculationTokenCount,
543
isNotebook: this.notebookService.hasSupportedNotebooks(request.uri) ? 1 : 0
544
});
545
}
546
547
private async mapCodeUsingFastEdit(request: ICodeMapperRequestInput, resultStream: MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: CancellationToken): Promise<CodeMapperOutcome | CodeMapperRefusal> {
548
// When generating edits for notebooks that are from location=panel, do not use fast edit.
549
// location = panel, is when user is applying code displayed in chat panel into notebook.
550
// Fast apply doesn't work well when we have only a part of the code and no code markers.
551
if (!request.createNew && request.location === 'panel' && this.notebookService.hasSupportedNotebooks(request.uri)) {
552
this.logService.error(`srequest | refuse | SD | refusing notebook from Panel | [codeMapper]`);
553
return new CodeMapperRefusal();
554
}
555
556
const combinedDocumentLength = isNewDocument(request) ? request.workingSet.reduce((prev, curr) => prev + curr.getText().length, 0) : request.existingDocument.getText().length;
557
558
const promptLimit = 256_000; // (256K is roughly 64k tokens) and documents longer than this will surely not fit
559
if (combinedDocumentLength > promptLimit) {
560
this.logService.error(`srequest | refuse | SD | refusing huge document | [codeMapper]`);
561
return new CodeMapperRefusal();
562
}
563
564
const builtPrompt = await this.buildPrompt(request, token);
565
const { promptTokenCount, speculation, requestId, endpoint } = builtPrompt;
566
567
// `prompt` includes the whole document, the codeblock and some prosa. we leave space
568
// for the document again and the whole codeblock (assuming it's all insertions)
569
// const codeBlockTokenCount = promptTokenCount - speculationTokenCount;
570
// if (promptTokenCount > 128_000 - speculationTokenCount - codeBlockTokenCount) {
571
572
if (promptTokenCount > 64_000) {
573
this.logService.error(`srequest | refuse | SD | exceeds token limit | [codeMapper]`);
574
return new CodeMapperRefusal();
575
}
576
577
const mapper = endpoint.model;
578
const outcomeCorrelationTelemetry: CodeMapperOutcomeTelemetry = {
579
requestId: String(telemetryInfo?.chatRequestId),
580
requestSource: String(telemetryInfo?.chatRequestSource),
581
chatRequestModel: String(telemetryInfo?.chatRequestModel),
582
speculationRequestId: requestId,
583
mapper,
584
};
585
586
const res = await this.fetchNativePredictedOutputs(request, builtPrompt, resultStream, outcomeCorrelationTelemetry, token, true);
587
588
if (isCodeMapperOutcome(res)) {
589
return res;
590
}
591
592
const { allResponseText, finishReason, annotations, firstTokenTime, startTime } = res;
593
594
try {
595
this.ensureFinishReasonStopOrThrow(requestId, finishReason);
596
const response = { responseText: allResponseText.join(''), startTime, firstTokenTime, requestId };
597
await this.logDoneInfo(request, builtPrompt, response, outcomeCorrelationTelemetry, mapper, annotations);
598
if (telemetryInfo?.chatRequestId) {
599
const prompt = JSON.stringify(builtPrompt.messages);
600
this.editLogService.logSpeculationRequest(telemetryInfo.chatRequestId, request.uri, prompt, speculation, response.responseText);
601
this.multiFileEditInternalTelemetryService.storeEditPrompt({ prompt, uri: request.uri, isAgent: telemetryInfo.isAgent, document: request.existingDocument?.document }, { chatRequestId: telemetryInfo.chatRequestId, chatSessionId: telemetryInfo.chatSessionId, speculationRequestId: requestId, mapper });
602
}
603
return { annotations, telemetry: outcomeCorrelationTelemetry };
604
} catch (err) {
605
const annotations: OutcomeAnnotation[] = [{ label: err.message, message: `request failed`, severity: 'error' }];
606
let errorDetails: ChatErrorDetails | undefined;
607
if (err instanceof CompletionsFetchError) {
608
if (err.type === 'stop_content_filter') {
609
errorDetails = {
610
message: getFilteredMessage(FilterReason.Prompt),
611
responseIsFiltered: true
612
};
613
} else if (err.type === 'stop_length') {
614
errorDetails = {
615
message: l10n.t(`Sorry, the response hit the length limit. Please rephrase your prompt.`)
616
};
617
}
618
this.logError(request, builtPrompt, { startTime, firstTokenTime, requestId }, outcomeCorrelationTelemetry, mapper, err.type);
619
} else {
620
this.logError(request, builtPrompt, { startTime, firstTokenTime, requestId }, outcomeCorrelationTelemetry, mapper, err.message, err);
621
}
622
errorDetails = errorDetails ?? {
623
message: l10n.t(`Sorry, your request failed. Please try again. Request id: {0}`, requestId)
624
};
625
return { errorDetails, annotations, telemetry: outcomeCorrelationTelemetry };
626
}
627
}
628
629
private async sendModelResponseInternalAndEnhancedTelemetry(useGPT4oProxy: boolean, builtPrompt: IFullRewritePrompt, result: ISuccessfulRewriteInfo, outcomeTelemetry: CodeMapperOutcomeTelemetry, mapper: string) {
630
const payload = {
631
headerRequestId: builtPrompt.requestId,
632
baseModel: outcomeTelemetry.chatRequestModel,
633
providerId: mapper,
634
languageId: builtPrompt.languageId,
635
messageText: useGPT4oProxy ? JSON.stringify(builtPrompt.messages) : builtPrompt.prompt,
636
completionTextJson: result.allResponseText.join(''),
637
};
638
this.telemetryService.sendEnhancedGHTelemetryEvent('fastApply/successfulEdit', multiplexProperties(payload));
639
this.telemetryService.sendInternalMSFTTelemetryEvent('fastApply/successfulEdit', payload);
640
}
641
642
private async fetchNativePredictedOutputs(request: ICodeMapperRequestInput, builtPrompt: IFullRewritePrompt, resultStream: MappedEditsResponseStream, outcomeTelemetry: CodeMapperOutcomeTelemetry, token: CancellationToken, applyEdits: boolean): Promise<CodeMapperOutcome | ISuccessfulRewriteInfo> {
643
const { messages, speculation, requestId, endpoint } = builtPrompt;
644
const startTime = Date.now();
645
646
const fetchResult = await this.fetchAndContinueOnLengthError(endpoint, messages, speculation, request, resultStream, token, applyEdits);
647
648
if (fetchResult.result.type !== ChatFetchResponseType.Success) {
649
this.logError(request, builtPrompt, { startTime, firstTokenTime: fetchResult.firstTokenTime, requestId }, outcomeTelemetry, builtPrompt.endpoint.model, fetchResult.result.type);
650
return {
651
annotations: fetchResult.annotations,
652
telemetry: outcomeTelemetry,
653
errorDetails: { message: fetchResult.result.reason }
654
};
655
}
656
657
const res = { allResponseText: fetchResult.allResponseText, firstTokenTime: fetchResult.firstTokenTime, startTime, finishReason: Completion.FinishReason.Stop, annotations: fetchResult.annotations, requestId };
658
this.sendModelResponseInternalAndEnhancedTelemetry(true, builtPrompt, res, outcomeTelemetry, builtPrompt.endpoint.model);
659
return res;
660
}
661
662
private async fetchAndContinueOnLengthError(endpoint: ChatEndpoint, promptMessages: Raw.ChatMessage[], speculation: string, request: ICodeMapperRequestInput, resultStream: MappedEditsResponseStream, token: CancellationToken, applyEdits: boolean): Promise<ISpeculationFetchResult> {
663
const allResponseText: string[] = [];
664
let responseLength = 0;
665
let firstTokenTime: number = -1;
666
667
const existingDocument = request.existingDocument;
668
const documentLength = existingDocument ? existingDocument.getText().length : 0;
669
const uri = request.uri;
670
const maxLength = documentLength + request.codeBlock.length + 1000; // add 1000 to be safe
671
672
//const { codeBlock, uri, documentContext, markdownBeforeBlock } = codemapperRequestInput;
673
const pushedLines: string[] = [];
674
const fetchStreamSource = new FetchStreamSource();
675
const textStream = fetchStreamSource.stream.map((part) => part.delta.text);
676
677
let processPromise: Promise<unknown> | undefined;
678
if (applyEdits) {
679
processPromise = existingDocument instanceof NotebookDocumentSnapshot
680
? processFullRewriteNotebook(existingDocument.document, readLineByLine(textStream, token), resultStream, this.alternativeNotebookEditGenerator, { source: NotebookEditGenrationSource.codeMapperFastApply, model: endpoint.model, requestId: undefined }, token) // corrected parameter passing
681
: processFullRewriteStream(uri, existingDocument, readLineByLine(textStream, token), resultStream, token, pushedLines);
682
} else {
683
processPromise = textStream.toPromise();
684
}
685
686
while (true) {
687
const result = await endpoint.makeChatRequest(
688
'editingSession/speculate',
689
promptMessages,
690
async (text, _, delta) => {
691
if (firstTokenTime === -1) {
692
firstTokenTime = Date.now();
693
}
694
fetchStreamSource.update(text, delta);
695
allResponseText.push(delta.text);
696
responseLength += delta.text.length;
697
return undefined;
698
},
699
token,
700
ChatLocation.EditingSession,
701
undefined,
702
{ stream: true, temperature: 0, prediction: { type: 'content', content: speculation } }
703
);
704
705
706
if (result.type === ChatFetchResponseType.Length) {
707
if (responseLength > maxLength) {
708
fetchStreamSource.resolve();
709
await processPromise; // Flush all received text as edits to the response stream
710
this.logCodemapperLoopTelemetry(request, result, uri, endpoint.model, documentLength, responseLength, true);
711
return {
712
result, firstTokenTime, allResponseText, annotations: [{
713
label: 'codemapper loop', message: `Code mapper might be in a loop: Rewritten length: ${responseLength}, Document length: ${documentLength}, Code block length ${request.codeBlock.length}`, severity: 'error'
714
}]
715
};
716
}
717
718
const promptRenderer = PromptRenderer.create(
719
this.instantiationService,
720
endpoint,
721
CodeMapperFullRewritePrompt,
722
{ request, shouldTrimCodeBlocks: true, inProgressRewriteContent: result.truncatedValue } satisfies CodeMapperPromptProps
723
);
724
const response = await promptRenderer.render(undefined, token);
725
promptMessages = response.messages;
726
} else if (result.type === ChatFetchResponseType.Success) {
727
fetchStreamSource.resolve();
728
await processPromise; // Flush all received text as edits to the response stream
729
730
if (applyEdits && (!existingDocument || existingDocument instanceof TextDocumentSnapshot)) {
731
await handleTrailingLines(uri, existingDocument, resultStream, pushedLines, token);
732
}
733
this.logCodemapperLoopTelemetry(request, result, uri, endpoint.model, documentLength, responseLength, false);
734
return { result, firstTokenTime, allResponseText, annotations: [] };
735
} else {
736
// error or cancelled
737
fetchStreamSource.resolve();
738
await processPromise; // Flush all received text as edits to the response stream
739
740
return { result, firstTokenTime, allResponseText: [], annotations: [] };
741
}
742
743
}
744
}
745
746
private logCodemapperLoopTelemetry(request: ICodeMapperRequestInput, result: ChatResponse, uri: Uri, model: string, documentLength: number, responseLength: number, hasLoop: boolean) {
747
/* __GDPR__
748
"speculation.response.loop" : {
749
"owner": "joyceerhl",
750
"comment": "Report when the model appears to have gone into a loop.",
751
"hasLoop": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the model appears to have gone into a loop." },
752
"speculationRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },
753
"languageId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The language id of the document" },
754
"model": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The model used for this speculation request" },
755
"documentLength": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Length of original file", "isMeasurement": true },
756
"rewrittenLength": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Length of original file", "isMeasurement": true }
757
}
758
*/
759
this.telemetryService.sendMSFTTelemetryEvent('speculation.response.loop', {
760
speculationRequestId: result.requestId,
761
languageId: isNewDocument(request) ? getLanguageForResource(uri).languageId : request.existingDocument.languageId,
762
model,
763
hasLoop: String(hasLoop)
764
}, {
765
documentLength,
766
rewrittenLength: responseLength
767
});
768
}
769
770
private ensureFinishReasonStopOrThrow(requestId: string, finishReason: Completion.FinishReason | undefined) {
771
switch (finishReason) {
772
case undefined:
773
break;
774
case Completion.FinishReason.ContentFilter:
775
throw new CompletionsFetchError('stop_content_filter', requestId, 'Content filter');
776
case Completion.FinishReason.Length:
777
throw new CompletionsFetchError('stop_length', requestId, 'Length limit');
778
case Completion.FinishReason.Stop:
779
break; // No error for 'Stop' finish reason
780
default:
781
assertNever(finishReason);
782
}
783
}
784
785
//#endregion
786
}
787
788
function readLineByLine(source: AsyncIterable<string>, token: CancellationToken): AsyncIterable<LineOfText> {
789
return new AsyncIterableObject<LineOfText>(async (emitter) => {
790
const reader = new PartialAsyncTextReader(source[Symbol.asyncIterator]());
791
let previousLineWasEmpty = false; // avoid emitting a trailing empty line all the time
792
while (!reader.endOfStream) {
793
// Skip everything until we hit a fence
794
if (token.isCancellationRequested) {
795
break;
796
}
797
const line = (await reader.readLine()).replace(/\r$/g, '');
798
799
if (previousLineWasEmpty) {
800
// Emit the previous held back empty line
801
emitter.emitOne(new LineOfText(''));
802
}
803
804
if (line === '') {
805
// Hold back empty lines and emit them with the next iteration
806
previousLineWasEmpty = true;
807
} else {
808
previousLineWasEmpty = false;
809
emitter.emitOne(new LineOfText(line));
810
}
811
}
812
});
813
}
814
815
export interface ISuccessfulRewriteInfo {
816
allResponseText: string[];
817
firstTokenTime: number;
818
startTime: number;
819
finishReason: Completion.FinishReason;
820
annotations: OutcomeAnnotation[];
821
}
822
823
function isCodeMapperOutcome(thing: unknown): thing is CodeMapperOutcome {
824
return typeof thing === 'object' && !!thing && 'annotations' in thing && 'telemetry' in thing;
825
}
826
827
export interface CodeMapperOutcome {
828
readonly errorDetails?: ChatErrorDetails;
829
readonly annotations: OutcomeAnnotation[];
830
readonly telemetry?: CodeMapperOutcomeTelemetry;
831
}
832
833
export interface CodeMapperOutcomeTelemetry {
834
readonly requestId: string;
835
readonly requestSource: string;
836
readonly chatRequestModel?: string;
837
readonly speculationRequestId: string;
838
readonly mapper: 'fast' | 'fast-lora' | 'full' | 'patch' | string;
839
}
840
841
class CodeMapperRefusal {
842
843
}
844
845
interface ISpeculationFetchResult {
846
result: ChatResponse;
847
firstTokenTime: number;
848
allResponseText: string[];
849
annotations: OutcomeAnnotation[];
850
}
851
852
function getTrailingDocumentEmptyLineCount(document: TextDocumentSnapshot): number {
853
let trailingEmptyLines = 0;
854
for (let i = document.lineCount - 1; i >= 0; i--) {
855
const line = document.lineAt(i);
856
if (line.text.trim() === '') {
857
trailingEmptyLines++;
858
} else {
859
break;
860
}
861
}
862
return trailingEmptyLines;
863
}
864
865
export function getTrailingArrayEmptyLineCount(lines: readonly string[]): number {
866
let trailingEmptyLines = 0;
867
for (let i = lines.length - 1; i >= 0; i--) {
868
if (lines[i].trim() === '') {
869
trailingEmptyLines++;
870
} else {
871
break;
872
}
873
}
874
return trailingEmptyLines;
875
}
876
877
async function computeAdditionsAndDeletions(diffService: IDiffService, original: string, modified: string): Promise<{ addedLines: number; removedLines: number }> {
878
const diffResult = await diffService.computeDiff(original, modified, {
879
ignoreTrimWhitespace: true,
880
maxComputationTimeMs: 10000,
881
computeMoves: false
882
});
883
884
let addedLines = 0;
885
let removedLines = 0;
886
for (const change of diffResult.changes) {
887
removedLines += change.original.endLineNumberExclusive - change.original.startLineNumber;
888
addedLines += change.modified.endLineNumberExclusive - change.modified.startLineNumber;
889
}
890
891
return { addedLines, removedLines };
892
}
893
894