Path: blob/main/extensions/copilot/src/extension/intents/node/newNotebookIntent.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as l10n from '@vscode/l10n';5import { Raw } from '@vscode/prompt-tsx';6import type { CancellationToken, ChatResponseFileTreePart, ChatResponseStream, NotebookDocument } from 'vscode';7import { IChatMLFetcher, IResponsePart } from '../../../platform/chat/common/chatMLFetcher';8import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';9import { IConversationOptions } from '../../../platform/chat/common/conversationOptions';10import { ILogService } from '../../../platform/log/common/logService';11import { IResponseDelta } from '../../../platform/networking/common/fetch';12import { IChatEndpoint } from '../../../platform/networking/common/networking';13import { IAlternativeNotebookContentEditGenerator, NotebookEditGenerationTelemtryOptions, NotebookEditGenrationSource } from '../../../platform/notebook/common/alternativeContentEditGenerator';14import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';15import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';16import { extractCodeBlocks, filepathCodeBlockMarker } from '../../../util/common/markdown';17import { extractNotebookOutline, INotebookSection } from '../../../util/common/notebooks';18import { AsyncIterableObject, AsyncIterableSource, DeferredPromise } from '../../../util/vs/base/common/async';19import { Lazy } from '../../../util/vs/base/common/lazy';20import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';21import { ChatResponseMarkdownPart, NotebookEdit, Uri, WorkspaceEdit } from '../../../vscodeTypes';22import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';23import { Turn } from '../../prompt/common/conversation';24import { IBuildPromptContext } from '../../prompt/common/intents';25import { IResponseProcessorContext } from '../../prompt/node/intents';26import { LineFilters, LineOfText, streamLines } from '../../prompt/node/streamingEdits';27import { renderPromptElement } from '../../prompts/node/base/promptRenderer';28import { NewNotebookCodeGenerationPrompt, NewNotebookCodeImprovementPrompt } from '../../prompts/node/panel/newNotebook';29import { sendEditNotebookTelemetry } from '../../tools/node/editNotebookTool';30import { NewNotebookToolPrompt } from '../../tools/node/newNotebookTool';313233export class NewNotebookResponseProcessor {3435private messageText = '';36private stagedTextToApply = '';37private reporting = true;38private _resolvedContentDeferredPromise: DeferredPromise<ChatResponseFileTreePart | ChatResponseMarkdownPart> = new DeferredPromise();3940constructor(41readonly endpoint: IChatEndpoint,42readonly context: IBuildPromptContext | undefined,43@IInstantiationService private readonly instantiationService: IInstantiationService,44@IWorkspaceService private readonly workspaceService: IWorkspaceService,45@IAlternativeNotebookContentEditGenerator private readonly noteBookEditGenerator: IAlternativeNotebookContentEditGenerator,46@ILogService private readonly logService: ILogService,47@ITelemetryService private readonly telemetryService: ITelemetryService48) { }4950async processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: ChatResponseStream, token: CancellationToken): Promise<void> {51const { turn, messages } = context;52for await (const { delta } of inputStream) {53if (token.isCancellationRequested) {54return;55}56this.applyDelta(delta.text, turn, outputStream);57}5859await this.pushCommands(messages, outputStream, token);60}6162private applyDeltaToTurn(textDelta: string, turn: Turn,) {63this.messageText += textDelta;64}6566private applyDeltaToProgress(textDelta: string, progress: ChatResponseStream) {67progress.markdown(textDelta);68}6970private _incodeblock = false;71private _presentCodeblockProgress = false;7273private applyDelta(textDelta: string, turn: Turn, progress: ChatResponseStream) {74if (!this.reporting) {75this.applyDeltaToTurn(textDelta, turn);76return;77}7879textDelta = this.stagedTextToApply + textDelta;8081if (this._incodeblock) {82const codeblockEnd = textDelta.indexOf('```');83if (codeblockEnd === -1) {84// didn't find closing yet85this.stagedTextToApply = textDelta;8687this.applyDeltaToTurn('', turn);88if (!this._presentCodeblockProgress) {89this._presentCodeblockProgress = true;90progress.progress(l10n.t('Thinking ...'));91}92return;93} else {94// found closing95this._incodeblock = false;96textDelta = textDelta.substring(0, codeblockEnd) + '```';97try {98this.applyDeltaToTurn(textDelta, turn);99} catch (_e) {100// const errorMessage = (e as Error)?.message ?? 'Unknown error';101} finally {102this.reporting = false;103this.stagedTextToApply = '';104this._resolvedContentDeferredPromise.complete(new ChatResponseMarkdownPart(''));105}106return;107}108}109110const codeblockStart = textDelta.indexOf('```');111if (codeblockStart !== -1) {112this._incodeblock = true;113const codeblockEnd = textDelta.indexOf('```', codeblockStart + 3);114if (codeblockEnd !== -1) {115this._incodeblock = false;116this.applyDeltaToProgress(textDelta.substring(0, codeblockStart), progress);117this.applyDeltaToProgress(textDelta.substring(codeblockEnd + 3), progress);118this.applyDeltaToTurn(textDelta, turn);119this.reporting = false;120this.stagedTextToApply = '';121122return;123} else {124const textToReport = textDelta.substring(0, codeblockStart);125this.applyDeltaToProgress(textToReport, progress);126this.applyDeltaToTurn(textDelta, turn);127this.stagedTextToApply = '';128129if (!this._presentCodeblockProgress) {130this._presentCodeblockProgress = true;131progress.progress(l10n.t('Thinking ...'));132}133134this._resolvedContentDeferredPromise.p.then((p) => progress.push(p));135return;136}137}138139// We have no stop word or partial, so apply the text to the progress and turn140this.applyDeltaToProgress(textDelta, progress);141this.applyDeltaToTurn(textDelta, turn);142this.stagedTextToApply = '';143}144145async pushCommands(history: readonly Raw.ChatMessage[], outputStream: ChatResponseStream, token: CancellationToken) {146try {147const outline = extractNotebookOutline(this.messageText);148if (outline) {149150const mockContext: IBuildPromptContext = this.context ?? {151query: '',152history: [],153chatVariables: new ChatVariablesCollection([]),154};155156const { messages: generateMessages } = await renderPromptElement(157this.instantiationService,158this.endpoint,159NewNotebookToolPrompt,160{161outline: outline,162promptContext: mockContext,163originalCreateNotebookQuery: mockContext.query,164availableTools: this.context?.tools?.availableTools165}166);167168const sourceStream = new AsyncIterableSource<string>();169const newNotebook = new Lazy(async () => {170const notebook = await this.workspaceService.openNotebookDocument('jupyter-notebook');171const updateMetadata = NotebookEdit.updateNotebookMetadata(Object.assign({ new_copilot_notebook: true }, notebook.metadata));172const workspaceEdit = new WorkspaceEdit();173workspaceEdit.set(notebook.uri, [updateMetadata]);174await this.workspaceService.applyEdit(workspaceEdit);175return notebook;176});177const sourceLines = filterFilePathFromCodeBlock2(streamLines(sourceStream.asyncIterable)178.filter(LineFilters.createCodeBlockFilter())179.map(line => {180// eslint-disable-next-line local/code-no-unused-expressions181newNotebook.value; // force the notebook to be created182return line;183}));184const created = this.createNewNotebook2(sourceLines, newNotebook.value, token);185async function finishedCb(text: string, index: number, delta: IResponseDelta): Promise<number | undefined> {186sourceStream.emitOne(delta.text);187return undefined;188}189190const generateResponse = await this.endpoint.makeChatRequest(191'newNotebookCodeCell',192generateMessages,193finishedCb,194token,195ChatLocation.Panel196);197sourceStream.resolve();198if (generateResponse.type !== ChatFetchResponseType.Success) {199return [];200}201await created;202} else {203this.logService.error('No Notebook outline found: ', this.messageText);204}205} catch (ex) {206this.logService.error('Error creating new notebook: ', ex);207}208209return [];210}211212// different than the one in the tool, this one can't rely on the output stream workaround213private async createNewNotebook2(lines: AsyncIterable<LineOfText>, newNotebook: Promise<NotebookDocument>, token: CancellationToken) {214const promises: Promise<unknown>[] = [];215const telemetryOptions: NotebookEditGenerationTelemtryOptions = {216source: NotebookEditGenrationSource.newNotebookIntent,217requestId: this.context?.requestId,218model: this.endpoint.model219};220for await (const edit of this.noteBookEditGenerator.generateNotebookEdits(Uri.file('empty.ipynb'), lines, telemetryOptions, token)) {221if (!Array.isArray(edit)) {222const notebook = await newNotebook;223const workspaceEdit = new WorkspaceEdit();224workspaceEdit.set(notebook.uri, [edit]);225promises.push(Promise.resolve(this.workspaceService.applyEdit(workspaceEdit)));226}227}228await Promise.all(promises);229sendEditNotebookTelemetry(this.telemetryService, undefined, 'newNotebookIntent', (await newNotebook).uri, this.context?.requestId, undefined, this.endpoint);230return newNotebook;231}232}233234// different than the one in the tool, this one can't rely on the output stream workaround235function filterFilePathFromCodeBlock2(source: AsyncIterable<LineOfText>): AsyncIterable<LineOfText> {236return new AsyncIterableObject<LineOfText>(async (emitter) => {237let index = -1;238for await (const line of source) {239index += 1;240if (index === 0 && line.value.includes(filepathCodeBlockMarker)) {241continue;242}243emitter.emitOne(line);244}245});246}247248// @Yoyokrazy -- look at removing the following fn's as debt. newNb is likely not needed at minimum249export async function newNotebookCodeCell(instantiationService: IInstantiationService, chatMLFetcher: IChatMLFetcher, endpoint: IChatEndpoint, options: IConversationOptions, history: readonly Raw.ChatMessage[] | undefined, description: string, section: INotebookSection, existingCode: string, languageId: string, uri: Uri, token: CancellationToken) {250const { messages } = await renderPromptElement(instantiationService, endpoint, NewNotebookCodeGenerationPrompt, {251history,252description,253section,254existingCode,255languageId,256uri257});258259const modelResponse = await endpoint.makeChatRequest(260'newNotebookCodeCell',261messages,262undefined,263token,264ChatLocation.Panel265);266if (modelResponse.type !== ChatFetchResponseType.Success) {267return;268}269270const codeBlocks = extractCodeBlocks(modelResponse.value);271272if (codeBlocks.length > 0) {273return codeBlocks[0].code;274}275276return modelResponse.value;277}278279export async function improveNotebookCodeCell(instantiationService: IInstantiationService, chatMLFetcher: IChatMLFetcher, endpoint: IChatEndpoint, options: IConversationOptions, description: string, section: INotebookSection, existingCode: string, code: string, languageId: string, uri: Uri, token: CancellationToken) {280281const { messages } = await renderPromptElement(instantiationService, endpoint, NewNotebookCodeImprovementPrompt, {282description,283section,284existingCode,285code,286languageId,287uri288});289290const modelResponse = await endpoint.makeChatRequest(291'improveNotebookCodeCell',292messages,293undefined,294token,295ChatLocation.Panel296);297if (modelResponse.type !== ChatFetchResponseType.Success) {298return;299}300301return modelResponse.value;302}303304305