Path: blob/main/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts
13405 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*--------------------------------------------------------------------------------------------*/45import * as l10n from '@vscode/l10n';6import { Raw } from '@vscode/prompt-tsx';7import type { ChatErrorDetails, MappedEditsResponseStream, NotebookCell, NotebookDocument, Uri } from 'vscode';8import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';9import { FetchStreamSource, IResponsePart } from '../../../../platform/chat/common/chatMLFetcher';10import { ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError, getFilteredMessage } from '../../../../platform/chat/common/commonTypes';11import { getTextPart, toTextPart } from '../../../../platform/chat/common/globalStringUtils';12import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';13import { IDiffService } from '../../../../platform/diff/common/diffService';14import { NotebookDocumentSnapshot } from '../../../../platform/editing/common/notebookDocumentSnapshot';15import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';16import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';17import { ChatEndpoint } from '../../../../platform/endpoint/node/chatEndpoint';18import { Proxy4oEndpoint } from '../../../../platform/endpoint/node/proxy4oEndpoint';19import { ProxyInstantApplyShortEndpoint } from '../../../../platform/endpoint/node/proxyInstantApplyShortEndpoint';20import { IOctoKitService } from '../../../../platform/github/common/githubService';21import { ILogService } from '../../../../platform/log/common/logService';22import { IEditLogService } from '../../../../platform/multiFileEdit/common/editLogService';23import { IMultiFileEditInternalTelemetryService } from '../../../../platform/multiFileEdit/common/multiFileEditQualityTelemetry';24import { Completion } from '../../../../platform/nesFetch/common/completionsAPI';25import { CompletionsFetchError } from '../../../../platform/nesFetch/common/completionsFetchService';26import { FinishedCallback, IResponseDelta } from '../../../../platform/networking/common/fetch';27import { FilterReason } from '../../../../platform/networking/common/openai';28import { IAlternativeNotebookContentEditGenerator, NotebookEditGenerationTelemtryOptions, NotebookEditGenrationSource } from '../../../../platform/notebook/common/alternativeContentEditGenerator';29import { INotebookService } from '../../../../platform/notebook/common/notebookService';30import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';31import { NullTelemetryService } from '../../../../platform/telemetry/common/nullTelemetryService';32import { ITelemetryService, multiplexProperties } from '../../../../platform/telemetry/common/telemetry';33import { ITokenizerProvider } from '../../../../platform/tokenizer/node/tokenizer';34import { getLanguageForResource } from '../../../../util/common/languages';35import { getFenceForCodeBlock, languageIdToMDCodeBlockLang } from '../../../../util/common/markdown';36import { ITokenizer } from '../../../../util/common/tokenizer';37import { equals } from '../../../../util/vs/base/common/arrays';38import { assertNever } from '../../../../util/vs/base/common/assert';39import { AsyncIterableObject } from '../../../../util/vs/base/common/async';40import { CancellationToken } from '../../../../util/vs/base/common/cancellation';41import { ResourceMap } from '../../../../util/vs/base/common/map';42import { isEqual } from '../../../../util/vs/base/common/resources';43import { URI } from '../../../../util/vs/base/common/uri';44import { generateUuid } from '../../../../util/vs/base/common/uuid';45import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';46import { NotebookEdit, Position, Range, TextEdit } from '../../../../vscodeTypes';47import { OutcomeAnnotation, OutcomeAnnotationLabel } from '../../../inlineChat/node/promptCraftingTypes';48import { Lines, LinesEdit } from '../../../prompt/node/editGeneration';49import { LineOfText, PartialAsyncTextReader } from '../../../prompt/node/streamingEdits';50import { PromptRenderer } from '../../../prompts/node/base/promptRenderer';51import { EXISTING_CODE_MARKER } from '../panel/codeBlockFormattingRules';52import { CodeMapperFullRewritePrompt, CodeMapperPatchRewritePrompt, CodeMapperPromptProps } from './codeMapperPrompt';53import { ICodeMapperTelemetryInfo } from './codeMapperService';54import { findEdit, getCodeBlock, iterateSectionsForResponse, Marker, Patch, Section } from './patchEditGeneration';555657export type ICodeMapperDocument = TextDocumentSnapshot | NotebookDocumentSnapshot;5859export async function processFullRewriteNotebook(document: NotebookDocument, inputStream: string | AsyncIterable<LineOfText>, outputStream: MappedEditsResponseStream, alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, telemetryOptions: NotebookEditGenerationTelemtryOptions, token: CancellationToken): Promise<void> {60for await (const edit of processFullRewriteNotebookEdits(document, inputStream, alternativeNotebookEditGenerator, telemetryOptions, token)) {61if (Array.isArray(edit)) {62outputStream.textEdit(edit[0], edit[1]);63} else {64outputStream.notebookEdit(document.uri, edit); // changed this.outputStream to outputStream65}66}6768return undefined;69}7071export type CellOrNotebookEdit = NotebookEdit | [Uri, TextEdit[]];7273export async function* processFullRewriteNotebookEdits(document: NotebookDocument, inputStream: string | AsyncIterable<LineOfText>, alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, telemetryOptions: NotebookEditGenerationTelemtryOptions, token: CancellationToken): AsyncIterable<CellOrNotebookEdit> {74// emit start of notebook75const cellMap = new ResourceMap<NotebookCell>();76for await (const edit of alternativeNotebookEditGenerator.generateNotebookEdits(document, inputStream, telemetryOptions, token)) {77if (Array.isArray(edit)) {78const cellUri = edit[0];79const cell = cellMap.get(cellUri) || document.getCells().find(cell => isEqual(cell.document.uri, cellUri));80if (cell) {81cellMap.set(cellUri, cell);82if (edit[1].length === 1 && edit[1][0].range.isSingleLine && cell.document.lineCount > edit[1][0].range.start.line) {83if (cell.document.lineAt(edit[1][0].range.start.line).text === edit[1][0].newText) {84continue;85}86}87yield [cellUri, edit[1]];88}89} else {90yield edit;91}92}9394return undefined;95}9697export async function processFullRewriteNewNotebook(uri: URI, source: string, outputStream: MappedEditsResponseStream, alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator, telemetryOptions: NotebookEditGenerationTelemtryOptions, token: CancellationToken): Promise<void> {98for await (const edit of alternativeNotebookEditGenerator.generateNotebookEdits(uri, source, telemetryOptions, token)) {99if (!Array.isArray(edit)) {100outputStream.notebookEdit(uri, edit);101}102}103104return undefined;105}106107function emitCodeLine(line: string, uri: Uri, existingDocument: TextDocumentSnapshot | undefined, outputStream: MappedEditsResponseStream, pushedLines: string[], token: CancellationToken) {108if (token.isCancellationRequested) {109return undefined;110}111112const lineCount = existingDocument ? existingDocument.lineCount : 0;113const currentLineIndex = pushedLines.length;114pushedLines.push(line);115116if (currentLineIndex < lineCount) {117// this line exists in the doc => replace it118const currentLineLength = existingDocument ? existingDocument.lineAt(currentLineIndex).text.length : 0;119outputStream.textEdit(uri, [TextEdit.replace(new Range(currentLineIndex, 0, currentLineIndex, currentLineLength), line)]);120} else {121// we are at the end of the document122const addedText = currentLineIndex === 0 ? line : `\n` + line;123outputStream.textEdit(uri, [TextEdit.replace(new Range(currentLineIndex, 0, currentLineIndex, 0), addedText)]);124}125}126127async function processFullRewriteStream(uri: Uri, existingDocument: TextDocumentSnapshot | undefined, inputStream: AsyncIterable<LineOfText>, outputStream: MappedEditsResponseStream, token: CancellationToken, pushedLines: string[] = []) {128for await (const line of inputStream) {129emitCodeLine(line.value, uri, existingDocument, outputStream, pushedLines, token);130}131132return pushedLines;133}134135async function handleTrailingLines(uri: Uri, existingDocument: TextDocumentSnapshot | undefined, outputStream: MappedEditsResponseStream, pushedLines: string[], token: CancellationToken): Promise<void> {136const lineCount = existingDocument ? existingDocument.lineCount : 0;137const initialTrailingEmptyLineCount = existingDocument ? getTrailingDocumentEmptyLineCount(existingDocument) : 0;138139// The LLM does not want to produce trailing newlines140// Here we try to maintain the exact same tralining newlines count as the original document had141const pushedTrailingEmptyLineCount = getTrailingArrayEmptyLineCount(pushedLines);142for (let i = pushedTrailingEmptyLineCount; i < initialTrailingEmptyLineCount; i++) {143emitCodeLine('', uri, existingDocument, outputStream, pushedLines, token);144}145146// Make sure we delete everything after the changed lines147const currentLineIndex = pushedLines.length;148if (currentLineIndex < lineCount) {149const from = currentLineIndex === 0 ? new Position(0, 0) : new Position(currentLineIndex - 1, pushedLines[pushedLines.length - 1].length);150outputStream.textEdit(uri, [TextEdit.delete(new Range(from, new Position(lineCount, 0)))]);151}152}153154async function processFullRewriteResponseCode(uri: Uri, existingDocument: TextDocumentSnapshot | undefined, inputStream: AsyncIterable<LineOfText>, outputStream: MappedEditsResponseStream, token: CancellationToken): Promise<void> {155const pushedLines = await processFullRewriteStream(uri, existingDocument, inputStream, outputStream, token);156157if (token.isCancellationRequested) {158return;159}160161await handleTrailingLines(uri, existingDocument, outputStream, pushedLines, token);162}163164/**165* Extract a fenced code block from a reply and emit the lines in the code block one-by-one.166*/167function extractCodeBlock(inputStream: AsyncIterable<IResponsePart>, token: CancellationToken): AsyncIterable<LineOfText> {168return new AsyncIterableObject<LineOfText>(async (emitter) => {169const fence = '```';170const textStream = AsyncIterableObject.map(inputStream, part => part.delta.text);171const reader = new PartialAsyncTextReader(textStream[Symbol.asyncIterator]());172173let inCodeBlock = false;174while (!reader.endOfStream) {175// Skip everything until we hit a fence176if (token.isCancellationRequested) {177break;178}179const line = await reader.readLine();180if (line.startsWith(fence) && inCodeBlock) {181// Done reading code block, stop reading182inCodeBlock = false;183break;184} else if (line.startsWith(fence)) {185inCodeBlock = true;186} else if (inCodeBlock) {187emitter.emitOne(new LineOfText(line));188}189}190});191}192193export async function processPatchResponse(uri: URI, originalText: string | undefined, inputStream: AsyncIterable<IResponsePart>, outputStream: MappedEditsResponseStream, token: CancellationToken): Promise<void> {194let documentLines = originalText ? Lines.fromString(originalText) : [];195function processAndEmitPatch(patch: Patch) {196// Make sure it's valid, otherwise emit197if (equals(patch.find, patch.replace)) {198return;199}200const res = findEdit(documentLines, getCodeBlock(patch.find), getCodeBlock(patch.replace), 0);201202if (res instanceof LinesEdit) {203outputStream.textEdit(uri, res.toTextEdit());204documentLines = res.apply(documentLines);205}206}207208let original, filePath;209const otherSections: Section[] = [];210for await (const section of iterateSectionsForResponse(inputStream)) {211switch (section.marker) {212case undefined:213break;214case Marker.FILEPATH:215filePath = section.content.join('\n').trim();216break;217case Marker.FIND:218original = section.content;219break;220case Marker.REPLACE: {221if (section.content && original && filePath) {222processAndEmitPatch({ filePath, find: original, replace: section.content });223}224break;225}226case Marker.COMPLETE:227break;228default:229otherSections.push(section);230break;231}232}233}234235export interface ICodeMapperNewDocument {236readonly createNew: true;237readonly codeBlock: string;238readonly markdownBeforeBlock: string | undefined;239readonly uri: Uri;240readonly existingDocument: ICodeMapperDocument | undefined;241readonly workingSet: ICodeMapperDocument[];242}243244export interface ICodeMapperExistingDocument {245readonly createNew: false;246readonly codeBlock: string;247readonly markdownBeforeBlock: string | undefined;248readonly uri: Uri;249readonly existingDocument: ICodeMapperDocument;250readonly location?: string;251}252253export type ICodeMapperRequestInput = ICodeMapperNewDocument | ICodeMapperExistingDocument;254255export function isNewDocument(input: ICodeMapperRequestInput): input is ICodeMapperNewDocument {256return input.createNew;257}258259interface IFullRewritePrompt {260readonly prompt: string;261readonly messages: Raw.ChatMessage[];262263readonly requestId: string;264265readonly languageId: string;266267readonly speculation: string;268readonly stopTokens: string[];269270readonly promptTokenCount: number;271readonly speculationTokenCount: number;272273readonly endpoint: ChatEndpoint;274readonly tokenizer: ITokenizer;275}276277interface ICompletedRequest {278readonly startTime: number;279readonly firstTokenTime: number;280readonly responseText: string;281readonly requestId: string;282}283284export class CodeMapper {285286static closingXmlTag = 'copilot-edited-file';287private shortContextLimit: number;288289constructor(290@IEndpointProvider private readonly endpointProvider: IEndpointProvider,291@IInstantiationService private readonly instantiationService: IInstantiationService,292@ITokenizerProvider private readonly tokenizerProvider: ITokenizerProvider,293@ILogService private readonly logService: ILogService,294@ITelemetryService private readonly telemetryService: ITelemetryService,295@IEditLogService private readonly editLogService: IEditLogService,296@IExperimentationService private readonly experimentationService: IExperimentationService,297@IDiffService private readonly diffService: IDiffService,298@IMultiFileEditInternalTelemetryService private readonly multiFileEditInternalTelemetryService: IMultiFileEditInternalTelemetryService,299@IAlternativeNotebookContentEditGenerator private readonly alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator,300@IAuthenticationService private readonly authenticationService: IAuthenticationService,301@IOctoKitService private readonly octoKitService: IOctoKitService,302@INotebookService private readonly notebookService: INotebookService,303@IConfigurationService configurationService: IConfigurationService,304) {305this.shortContextLimit = configurationService.getExperimentBasedConfig<number>(ConfigKey.Advanced.InstantApplyShortContextLimit, experimentationService) ?? 8000;306}307308private async getGpt4oProxyEndpoint(): Promise<Proxy4oEndpoint> {309await this.experimentationService.hasTreatments();310return this.instantiationService.createInstance(Proxy4oEndpoint);311}312313private async getShortIAEndpoint(): Promise<ProxyInstantApplyShortEndpoint> {314await this.experimentationService.hasTreatments();315return this.instantiationService.createInstance(ProxyInstantApplyShortEndpoint);316}317318public async mapCode(request: ICodeMapperRequestInput, resultStream: MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: CancellationToken): Promise<CodeMapperOutcome | undefined> {319320const fastEdit = await this.mapCodeUsingFastEdit(request, resultStream, telemetryInfo, token);321if (!(fastEdit instanceof CodeMapperRefusal)) {322return fastEdit;323}324// continue with "slow rewrite endpoint" when fast rewriting was not possible325// use copilot base as fallback326const chatEndpoint = await this.endpointProvider.getChatEndpoint('copilot-base');327328// 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.329// If there are multiple input documents, pick the longest one to base the limit on330const longestDocumentContext = isNewDocument(request) ? request.workingSet.reduce<ICodeMapperDocument | undefined>((prev, curr) => (prev && (prev.getText().length > curr.getText().length)) ? prev : curr, undefined) : request.existingDocument;331const doFullRewrite = longestDocumentContext ? await chatEndpoint.acquireTokenizer().tokenLength(longestDocumentContext.getText()) < (4096 / 4 * 3) : true;332333const existingDocument = request.existingDocument;334335const fetchStreamSource = new FetchStreamSource();336337const cb: FinishedCallback = async (text: string, index: number, delta: IResponseDelta) => {338fetchStreamSource.update(text, delta);339return undefined;340};341let responsePromise: Promise<void> | undefined;342if (doFullRewrite) {343if (existingDocument && existingDocument instanceof NotebookDocumentSnapshot) { // TODO@joyceerhl: Handle notebook document response processing344const telemtryOptions: NotebookEditGenerationTelemtryOptions = {345source: NotebookEditGenrationSource.codeMapperEditNotebook,346requestId: undefined,347model: chatEndpoint.model348};349responsePromise = processFullRewriteNotebook(existingDocument.document, extractCodeBlock(fetchStreamSource.stream, token), resultStream, this.alternativeNotebookEditGenerator, telemtryOptions, token);350} else {351responsePromise = processFullRewriteResponseCode(request.uri, existingDocument, extractCodeBlock(fetchStreamSource.stream, token), resultStream, token);352}353} else {354responsePromise = processPatchResponse(request.uri, existingDocument?.getText(), fetchStreamSource.stream, resultStream, token);355}356357const promptRenderer = PromptRenderer.create(358this.instantiationService,359chatEndpoint,360doFullRewrite ? CodeMapperFullRewritePrompt : CodeMapperPatchRewritePrompt,361{ request } satisfies CodeMapperPromptProps362);363364const prompt = await promptRenderer.render(undefined, token);365if (token.isCancellationRequested) {366return undefined;367}368const fetchResult = await chatEndpoint.makeChatRequest(369'codeMapper',370prompt.messages,371cb,372token,373ChatLocation.Other,374undefined,375{ temperature: 0 }376);377378fetchStreamSource.resolve();379await responsePromise; // Make sure we push all text edits to the response stream380381let result: CodeMapperOutcome;382383const createOutcome = (annotations: OutcomeAnnotation[], errorDetails: ChatErrorDetails | undefined): CodeMapperOutcome => {384return ({ errorDetails, annotations, telemetry: { requestId: String(telemetryInfo?.chatRequestId), speculationRequestId: fetchResult.requestId, requestSource: String(telemetryInfo?.chatRequestSource), mapper: doFullRewrite ? 'full' : 'patch' } });385};386if (fetchResult.type === ChatFetchResponseType.Success) {387result = createOutcome([], undefined);388} else {389if (fetchResult.type === ChatFetchResponseType.Canceled) {390return undefined;391}392const outageStatus = await this.octoKitService.getGitHubOutageStatus();393const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this.authenticationService.getCopilotToken()).copilotPlan, outageStatus);394result = createOutcome([{ label: errorDetails.message, message: `request ${fetchResult.type}`, severity: 'error' }], errorDetails);395}396if (result.annotations.length || result.errorDetails) {397this.logService.info(`[code mapper] Problems generating edits: ${result.annotations.map(a => `${a.message} [${a.label}]`).join(', ')}, ${result.errorDetails?.message}`);398}399return result;400}401402//#region Full file rewrite with speculation / predicted outputs403404private async buildPrompt(request: ICodeMapperRequestInput, token: CancellationToken): Promise<IFullRewritePrompt> {405let endpoint: ChatEndpoint = await this.getGpt4oProxyEndpoint();406const tokenizer = this.tokenizerProvider.acquireTokenizer(endpoint);407const requestId = generateUuid();408409const promptRenderer = PromptRenderer.create(410this.instantiationService,411endpoint,412CodeMapperFullRewritePrompt,413{ request, shouldTrimCodeBlocks: true } satisfies CodeMapperPromptProps414);415const uri = request.uri;416417const promptRendererResult = await promptRenderer.render(undefined, token);418const fence = isNewDocument(request) ? '```' : getFenceForCodeBlock(request.existingDocument.getText());419const languageId = isNewDocument(request) ? getLanguageForResource(uri).languageId : request.existingDocument.languageId;420const speculation = isNewDocument(request) ? '' : request.existingDocument.getText();421const messages: Raw.ChatMessage[] = [{422role: Raw.ChatRole.User,423content: [toTextPart(promptRendererResult.messages.reduce((prev, curr) => {424const content = getTextPart(curr.content);425if (curr.role === Raw.ChatRole.System) {426const currentContent = content.endsWith('\n') ? content : `${content}\n`;427return `${prev}<SYSTEM>\n${currentContent}</SYSTEM>\n\n\n`;428}429return prev + content;430}, ''))]431}];432const prompt = promptRendererResult.messages.reduce((prev, curr) => {433const content = getTextPart(curr.content);434if (curr.role === Raw.ChatRole.System) {435const currentContent = content.endsWith('\n') ? content : `${content}\n`;436return `${prev}<SYSTEM>\n${currentContent}\nEnd your response with </${CodeMapper.closingXmlTag}>.\n</SYSTEM>\n\n\n`;437}438return prev + content;439}, '').trimEnd() + `\n\n\nThe resulting document:\n<${CodeMapper.closingXmlTag}>\n${fence}${languageIdToMDCodeBlockLang(languageId)}\n`;440441if (prompt.length < this.shortContextLimit) {442endpoint = await this.getShortIAEndpoint();443}444445const promptTokenCount = await tokenizer.tokenLength(prompt);446const speculationTokenCount = await tokenizer.tokenLength(speculation);447const stopTokens = [`${fence}\n</${CodeMapper.closingXmlTag}>`, `${fence}\r\n</${CodeMapper.closingXmlTag}>`, `</${CodeMapper.closingXmlTag}>`];448449return { prompt, requestId, messages, speculation, stopTokens, promptTokenCount, speculationTokenCount, endpoint, tokenizer, languageId };450}451452private async logDoneInfo(request: ICodeMapperRequestInput, prompt: IFullRewritePrompt, response: ICompletedRequest, telemetryInfo: CodeMapperOutcomeTelemetry, mapper: string, annotations: OutcomeAnnotation[]) {453if (this.telemetryService instanceof NullTelemetryService) {454// noo need to make all the computation455return;456}457458const { speculation, tokenizer, promptTokenCount, speculationTokenCount } = prompt;459const { firstTokenTime, startTime, responseText, requestId } = response;460461const timeToFirstToken = firstTokenTime === -1 ? -1 : firstTokenTime - startTime;462const timeToComplete = Date.now() - startTime;463this.logService.info(`srequest done: ${timeToComplete}ms, chatRequestId: [${telemetryInfo?.requestId}], speculationRequestId: [${requestId}]`);464const isNoopEdit = responseText.trim() === speculation.trim();465466const { addedLines, removedLines } = await computeAdditionsAndDeletions(this.diffService, speculation, responseText);467468/* __GDPR__469"speculation.response.success" : {470"owner": "alexdima",471"comment": "Report quality details for a successful speculative response.",472"chatRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },473"chatRequestSource": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Source of the current turn request" },474"isNoopEdit": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the response text is identical to the speculation." },475"speculationRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },476"containsElidedCodeComments": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the response text contains elided code comments." },477"model": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The model used for this speculation request" },478"promptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens", "isMeasurement": true },479"speculationTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of speculation tokens", "isMeasurement": true },480"responseTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of response tokens", "isMeasurement": true },481"addedLines": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of lines added", "isMeasurement": true },482"removedLines": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of lines removed", "isMeasurement": true },483"isNotebook": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether this is a notebook", "isMeasurement": true },484"timeToFirstToken": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to first token", "isMeasurement": true },485"timeToComplete": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to complete the request", "isMeasurement": true }486}487*/488this.telemetryService.sendMSFTTelemetryEvent('speculation.response.success', {489chatRequestId: telemetryInfo?.requestId,490chatRequestSource: telemetryInfo?.requestSource,491speculationRequestId: requestId,492isNoopEdit: String(isNoopEdit),493containsElidedCodeComments: String(responseText.includes(EXISTING_CODE_MARKER)),494model: mapper495}, {496promptTokenCount,497speculationTokenCount,498responseTokenCount: await tokenizer.tokenLength(responseText),499timeToFirstToken,500timeToComplete,501addedLines,502removedLines,503isNotebook: this.notebookService.hasSupportedNotebooks(request.uri) ? 1 : 0504});505if (isNoopEdit) {506const message = 'Speculative response is identical to speculation, srequest: ' + requestId + ', URI: ' + request.uri.toString();507annotations.push({ label: OutcomeAnnotationLabel.NOOP_EDITS, message, severity: 'error' });508}509}510511private async logError(request: ICodeMapperRequestInput, prompt: IFullRewritePrompt, response: Omit<ICompletedRequest, 'responseText'>, telemetryInfo: CodeMapperOutcomeTelemetry, mapper: string, errorMessage: string, error?: Error) {512const { promptTokenCount, speculationTokenCount } = prompt;513const { startTime, requestId } = response;514515this.logService.error(`srequest failed: ${Date.now() - startTime}ms, chatRequestId: [${telemetryInfo?.requestId}], speculationRequestId: [${requestId}] error: [${errorMessage}]`);516if (error) {517this.logService.error(error);518}519/* __GDPR__520"speculation.response.error" : {521"owner": "alexdima",522"comment": "Report quality issue for when a speculative response failed.",523"errorMessage": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The name of the error" },524"chatRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },525"speculationRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the speculation request" },526"chatRequestSource": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Source of the current turn request" },527"model": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The model used for this speculation request" },528"promptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens", "isMeasurement": true },529"speculationTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of speculation tokens", "isMeasurement": true },530"isNotebook": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether this is a notebook", "isMeasurement": true }531}532*/533this.telemetryService.sendMSFTTelemetryEvent('speculation.response.error', {534errorMessage,535chatRequestId: telemetryInfo?.requestId,536chatRequestSource: telemetryInfo?.requestSource,537speculationRequestId: requestId,538model: mapper539}, {540promptTokenCount,541speculationTokenCount,542isNotebook: this.notebookService.hasSupportedNotebooks(request.uri) ? 1 : 0543});544}545546private async mapCodeUsingFastEdit(request: ICodeMapperRequestInput, resultStream: MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: CancellationToken): Promise<CodeMapperOutcome | CodeMapperRefusal> {547// When generating edits for notebooks that are from location=panel, do not use fast edit.548// location = panel, is when user is applying code displayed in chat panel into notebook.549// Fast apply doesn't work well when we have only a part of the code and no code markers.550if (!request.createNew && request.location === 'panel' && this.notebookService.hasSupportedNotebooks(request.uri)) {551this.logService.error(`srequest | refuse | SD | refusing notebook from Panel | [codeMapper]`);552return new CodeMapperRefusal();553}554555const combinedDocumentLength = isNewDocument(request) ? request.workingSet.reduce((prev, curr) => prev + curr.getText().length, 0) : request.existingDocument.getText().length;556557const promptLimit = 256_000; // (256K is roughly 64k tokens) and documents longer than this will surely not fit558if (combinedDocumentLength > promptLimit) {559this.logService.error(`srequest | refuse | SD | refusing huge document | [codeMapper]`);560return new CodeMapperRefusal();561}562563const builtPrompt = await this.buildPrompt(request, token);564const { promptTokenCount, speculation, requestId, endpoint } = builtPrompt;565566// `prompt` includes the whole document, the codeblock and some prosa. we leave space567// for the document again and the whole codeblock (assuming it's all insertions)568// const codeBlockTokenCount = promptTokenCount - speculationTokenCount;569// if (promptTokenCount > 128_000 - speculationTokenCount - codeBlockTokenCount) {570571if (promptTokenCount > 64_000) {572this.logService.error(`srequest | refuse | SD | exceeds token limit | [codeMapper]`);573return new CodeMapperRefusal();574}575576const mapper = endpoint.model;577const outcomeCorrelationTelemetry: CodeMapperOutcomeTelemetry = {578requestId: String(telemetryInfo?.chatRequestId),579requestSource: String(telemetryInfo?.chatRequestSource),580chatRequestModel: String(telemetryInfo?.chatRequestModel),581speculationRequestId: requestId,582mapper,583};584585const res = await this.fetchNativePredictedOutputs(request, builtPrompt, resultStream, outcomeCorrelationTelemetry, token, true);586587if (isCodeMapperOutcome(res)) {588return res;589}590591const { allResponseText, finishReason, annotations, firstTokenTime, startTime } = res;592593try {594this.ensureFinishReasonStopOrThrow(requestId, finishReason);595const response = { responseText: allResponseText.join(''), startTime, firstTokenTime, requestId };596await this.logDoneInfo(request, builtPrompt, response, outcomeCorrelationTelemetry, mapper, annotations);597if (telemetryInfo?.chatRequestId) {598const prompt = JSON.stringify(builtPrompt.messages);599this.editLogService.logSpeculationRequest(telemetryInfo.chatRequestId, request.uri, prompt, speculation, response.responseText);600this.multiFileEditInternalTelemetryService.storeEditPrompt({ prompt, uri: request.uri, isAgent: telemetryInfo.isAgent, document: request.existingDocument?.document }, { chatRequestId: telemetryInfo.chatRequestId, chatSessionId: telemetryInfo.chatSessionId, speculationRequestId: requestId, mapper });601}602return { annotations, telemetry: outcomeCorrelationTelemetry };603} catch (err) {604const annotations: OutcomeAnnotation[] = [{ label: err.message, message: `request failed`, severity: 'error' }];605let errorDetails: ChatErrorDetails | undefined;606if (err instanceof CompletionsFetchError) {607if (err.type === 'stop_content_filter') {608errorDetails = {609message: getFilteredMessage(FilterReason.Prompt),610responseIsFiltered: true611};612} else if (err.type === 'stop_length') {613errorDetails = {614message: l10n.t(`Sorry, the response hit the length limit. Please rephrase your prompt.`)615};616}617this.logError(request, builtPrompt, { startTime, firstTokenTime, requestId }, outcomeCorrelationTelemetry, mapper, err.type);618} else {619this.logError(request, builtPrompt, { startTime, firstTokenTime, requestId }, outcomeCorrelationTelemetry, mapper, err.message, err);620}621errorDetails = errorDetails ?? {622message: l10n.t(`Sorry, your request failed. Please try again. Request id: {0}`, requestId)623};624return { errorDetails, annotations, telemetry: outcomeCorrelationTelemetry };625}626}627628private async sendModelResponseInternalAndEnhancedTelemetry(useGPT4oProxy: boolean, builtPrompt: IFullRewritePrompt, result: ISuccessfulRewriteInfo, outcomeTelemetry: CodeMapperOutcomeTelemetry, mapper: string) {629const payload = {630headerRequestId: builtPrompt.requestId,631baseModel: outcomeTelemetry.chatRequestModel,632providerId: mapper,633languageId: builtPrompt.languageId,634messageText: useGPT4oProxy ? JSON.stringify(builtPrompt.messages) : builtPrompt.prompt,635completionTextJson: result.allResponseText.join(''),636};637this.telemetryService.sendEnhancedGHTelemetryEvent('fastApply/successfulEdit', multiplexProperties(payload));638this.telemetryService.sendInternalMSFTTelemetryEvent('fastApply/successfulEdit', payload);639}640641private async fetchNativePredictedOutputs(request: ICodeMapperRequestInput, builtPrompt: IFullRewritePrompt, resultStream: MappedEditsResponseStream, outcomeTelemetry: CodeMapperOutcomeTelemetry, token: CancellationToken, applyEdits: boolean): Promise<CodeMapperOutcome | ISuccessfulRewriteInfo> {642const { messages, speculation, requestId, endpoint } = builtPrompt;643const startTime = Date.now();644645const fetchResult = await this.fetchAndContinueOnLengthError(endpoint, messages, speculation, request, resultStream, token, applyEdits);646647if (fetchResult.result.type !== ChatFetchResponseType.Success) {648this.logError(request, builtPrompt, { startTime, firstTokenTime: fetchResult.firstTokenTime, requestId }, outcomeTelemetry, builtPrompt.endpoint.model, fetchResult.result.type);649return {650annotations: fetchResult.annotations,651telemetry: outcomeTelemetry,652errorDetails: { message: fetchResult.result.reason }653};654}655656const res = { allResponseText: fetchResult.allResponseText, firstTokenTime: fetchResult.firstTokenTime, startTime, finishReason: Completion.FinishReason.Stop, annotations: fetchResult.annotations, requestId };657this.sendModelResponseInternalAndEnhancedTelemetry(true, builtPrompt, res, outcomeTelemetry, builtPrompt.endpoint.model);658return res;659}660661private async fetchAndContinueOnLengthError(endpoint: ChatEndpoint, promptMessages: Raw.ChatMessage[], speculation: string, request: ICodeMapperRequestInput, resultStream: MappedEditsResponseStream, token: CancellationToken, applyEdits: boolean): Promise<ISpeculationFetchResult> {662const allResponseText: string[] = [];663let responseLength = 0;664let firstTokenTime: number = -1;665666const existingDocument = request.existingDocument;667const documentLength = existingDocument ? existingDocument.getText().length : 0;668const uri = request.uri;669const maxLength = documentLength + request.codeBlock.length + 1000; // add 1000 to be safe670671//const { codeBlock, uri, documentContext, markdownBeforeBlock } = codemapperRequestInput;672const pushedLines: string[] = [];673const fetchStreamSource = new FetchStreamSource();674const textStream = fetchStreamSource.stream.map((part) => part.delta.text);675676let processPromise: Promise<unknown> | undefined;677if (applyEdits) {678processPromise = existingDocument instanceof NotebookDocumentSnapshot679? processFullRewriteNotebook(existingDocument.document, readLineByLine(textStream, token), resultStream, this.alternativeNotebookEditGenerator, { source: NotebookEditGenrationSource.codeMapperFastApply, model: endpoint.model, requestId: undefined }, token) // corrected parameter passing680: processFullRewriteStream(uri, existingDocument, readLineByLine(textStream, token), resultStream, token, pushedLines);681} else {682processPromise = textStream.toPromise();683}684685while (true) {686const result = await endpoint.makeChatRequest(687'editingSession/speculate',688promptMessages,689async (text, _, delta) => {690if (firstTokenTime === -1) {691firstTokenTime = Date.now();692}693fetchStreamSource.update(text, delta);694allResponseText.push(delta.text);695responseLength += delta.text.length;696return undefined;697},698token,699ChatLocation.EditingSession,700undefined,701{ stream: true, temperature: 0, prediction: { type: 'content', content: speculation } }702);703704705if (result.type === ChatFetchResponseType.Length) {706if (responseLength > maxLength) {707fetchStreamSource.resolve();708await processPromise; // Flush all received text as edits to the response stream709this.logCodemapperLoopTelemetry(request, result, uri, endpoint.model, documentLength, responseLength, true);710return {711result, firstTokenTime, allResponseText, annotations: [{712label: 'codemapper loop', message: `Code mapper might be in a loop: Rewritten length: ${responseLength}, Document length: ${documentLength}, Code block length ${request.codeBlock.length}`, severity: 'error'713}]714};715}716717const promptRenderer = PromptRenderer.create(718this.instantiationService,719endpoint,720CodeMapperFullRewritePrompt,721{ request, shouldTrimCodeBlocks: true, inProgressRewriteContent: result.truncatedValue } satisfies CodeMapperPromptProps722);723const response = await promptRenderer.render(undefined, token);724promptMessages = response.messages;725} else if (result.type === ChatFetchResponseType.Success) {726fetchStreamSource.resolve();727await processPromise; // Flush all received text as edits to the response stream728729if (applyEdits && (!existingDocument || existingDocument instanceof TextDocumentSnapshot)) {730await handleTrailingLines(uri, existingDocument, resultStream, pushedLines, token);731}732this.logCodemapperLoopTelemetry(request, result, uri, endpoint.model, documentLength, responseLength, false);733return { result, firstTokenTime, allResponseText, annotations: [] };734} else {735// error or cancelled736fetchStreamSource.resolve();737await processPromise; // Flush all received text as edits to the response stream738739return { result, firstTokenTime, allResponseText: [], annotations: [] };740}741742}743}744745private logCodemapperLoopTelemetry(request: ICodeMapperRequestInput, result: ChatResponse, uri: Uri, model: string, documentLength: number, responseLength: number, hasLoop: boolean) {746/* __GDPR__747"speculation.response.loop" : {748"owner": "joyceerhl",749"comment": "Report when the model appears to have gone into a loop.",750"hasLoop": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the model appears to have gone into a loop." },751"speculationRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" },752"languageId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The language id of the document" },753"model": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The model used for this speculation request" },754"documentLength": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Length of original file", "isMeasurement": true },755"rewrittenLength": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Length of original file", "isMeasurement": true }756}757*/758this.telemetryService.sendMSFTTelemetryEvent('speculation.response.loop', {759speculationRequestId: result.requestId,760languageId: isNewDocument(request) ? getLanguageForResource(uri).languageId : request.existingDocument.languageId,761model,762hasLoop: String(hasLoop)763}, {764documentLength,765rewrittenLength: responseLength766});767}768769private ensureFinishReasonStopOrThrow(requestId: string, finishReason: Completion.FinishReason | undefined) {770switch (finishReason) {771case undefined:772break;773case Completion.FinishReason.ContentFilter:774throw new CompletionsFetchError('stop_content_filter', requestId, 'Content filter');775case Completion.FinishReason.Length:776throw new CompletionsFetchError('stop_length', requestId, 'Length limit');777case Completion.FinishReason.Stop:778break; // No error for 'Stop' finish reason779default:780assertNever(finishReason);781}782}783784//#endregion785}786787function readLineByLine(source: AsyncIterable<string>, token: CancellationToken): AsyncIterable<LineOfText> {788return new AsyncIterableObject<LineOfText>(async (emitter) => {789const reader = new PartialAsyncTextReader(source[Symbol.asyncIterator]());790let previousLineWasEmpty = false; // avoid emitting a trailing empty line all the time791while (!reader.endOfStream) {792// Skip everything until we hit a fence793if (token.isCancellationRequested) {794break;795}796const line = (await reader.readLine()).replace(/\r$/g, '');797798if (previousLineWasEmpty) {799// Emit the previous held back empty line800emitter.emitOne(new LineOfText(''));801}802803if (line === '') {804// Hold back empty lines and emit them with the next iteration805previousLineWasEmpty = true;806} else {807previousLineWasEmpty = false;808emitter.emitOne(new LineOfText(line));809}810}811});812}813814export interface ISuccessfulRewriteInfo {815allResponseText: string[];816firstTokenTime: number;817startTime: number;818finishReason: Completion.FinishReason;819annotations: OutcomeAnnotation[];820}821822function isCodeMapperOutcome(thing: unknown): thing is CodeMapperOutcome {823return typeof thing === 'object' && !!thing && 'annotations' in thing && 'telemetry' in thing;824}825826export interface CodeMapperOutcome {827readonly errorDetails?: ChatErrorDetails;828readonly annotations: OutcomeAnnotation[];829readonly telemetry?: CodeMapperOutcomeTelemetry;830}831832export interface CodeMapperOutcomeTelemetry {833readonly requestId: string;834readonly requestSource: string;835readonly chatRequestModel?: string;836readonly speculationRequestId: string;837readonly mapper: 'fast' | 'fast-lora' | 'full' | 'patch' | string;838}839840class CodeMapperRefusal {841842}843844interface ISpeculationFetchResult {845result: ChatResponse;846firstTokenTime: number;847allResponseText: string[];848annotations: OutcomeAnnotation[];849}850851function getTrailingDocumentEmptyLineCount(document: TextDocumentSnapshot): number {852let trailingEmptyLines = 0;853for (let i = document.lineCount - 1; i >= 0; i--) {854const line = document.lineAt(i);855if (line.text.trim() === '') {856trailingEmptyLines++;857} else {858break;859}860}861return trailingEmptyLines;862}863864export function getTrailingArrayEmptyLineCount(lines: readonly string[]): number {865let trailingEmptyLines = 0;866for (let i = lines.length - 1; i >= 0; i--) {867if (lines[i].trim() === '') {868trailingEmptyLines++;869} else {870break;871}872}873return trailingEmptyLines;874}875876async function computeAdditionsAndDeletions(diffService: IDiffService, original: string, modified: string): Promise<{ addedLines: number; removedLines: number }> {877const diffResult = await diffService.computeDiff(original, modified, {878ignoreTrimWhitespace: true,879maxComputationTimeMs: 10000,880computeMoves: false881});882883let addedLines = 0;884let removedLines = 0;885for (const change of diffResult.changes) {886removedLines += change.original.endLineNumberExclusive - change.original.startLineNumber;887addedLines += change.modified.endLineNumberExclusive - change.modified.startLineNumber;888}889890return { addedLines, removedLines };891}892893894