Path: blob/main/extensions/copilot/src/extension/test/node/notebookPromptRendering.spec.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*--------------------------------------------------------------------------------------------*/45import { beforeAll, beforeEach, describe, expect, test } from 'vitest';6import type * as vscode from 'vscode';7import { getTextPart } from '../../../platform/chat/common/globalStringUtils';8import { CHAT_MODEL } from '../../../platform/configuration/common/configurationService';9import { DiffServiceImpl } from '../../../platform/diff/node/diffServiceImpl';10import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';11import { MockEndpoint } from '../../../platform/endpoint/test/node/mockEndpoint';12import { ILogger, ILogService } from '../../../platform/log/common/logService';13import { IChatEndpoint } from '../../../platform/networking/common/networking';14import { IAlternativeNotebookContentService } from '../../../platform/notebook/common/alternativeContent';15import { AlternativeNotebookContentEditGenerator, IAlternativeNotebookContentEditGenerator } from '../../../platform/notebook/common/alternativeContentEditGenerator';16import { INotebookService, PipPackage, VariablesResult } from '../../../platform/notebook/common/notebookService';17import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEditorsService';18import { IExperimentationService, NullExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';19import { NullTelemetryService } from '../../../platform/telemetry/common/nullTelemetryService';20import { ITestingServicesAccessor } from '../../../platform/test/node/services';21import { SimulationAlternativeNotebookContentService, TestingTabsAndEditorsService } from '../../../platform/test/node/simulationWorkspaceServices';22import { ITokenizerProvider } from '../../../platform/tokenizer/node/tokenizer';23import { AbstractWorkspaceService, IWorkspaceService } from '../../../platform/workspace/common/workspaceService';24import { ExtHostNotebookDocumentData } from '../../../util/common/test/shims/notebookDocument';25import { TokenizerType } from '../../../util/common/tokenizer';26import { CancellationToken } from '../../../util/vs/base/common/cancellation';27import { Event } from '../../../util/vs/base/common/event';28import { URI } from '../../../util/vs/base/common/uri';29import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';30import { NotebookCellData, NotebookCellKind, NotebookData, Range, Selection, Uri } from '../../../vscodeTypes';31import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';32import { IDocumentContext } from '../../prompt/node/documentContext';33import { PromptRenderer } from '../../prompts/node/base/promptRenderer';34import { InlineChatNotebookGeneratePrompt } from '../../prompts/node/inline/inlineChatNotebookGeneratePrompt';35import { createExtensionUnitTestingServices } from './services';3637function getFakeDocumentContext(notebook: vscode.NotebookDocument, index: number = 0) {38const cell = notebook.getCells()[index];39const docSnapshot = TextDocumentSnapshot.create(cell.document);4041const context: IDocumentContext = {42document: docSnapshot,43language: { languageId: docSnapshot.languageId, lineComment: { start: '//' } },44fileIndentInfo: undefined,45wholeRange: new Range(0, 0, 1, 0),46selection: new Selection(0, 0, 0, 0),47};4849return context;50}5152function getFakeNotebookEditor(): vscode.NotebookEditor {53const cells = [54new NotebookCellData(NotebookCellKind.Code, 'print("hello")', 'python'),55new NotebookCellData(NotebookCellKind.Code, 'print("world")', 'python'),56];57const uri = URI.from({ scheme: 'file', path: '/path/file.ipynb' });58const notebook = ExtHostNotebookDocumentData.fromNotebookData(uri, new NotebookData(cells), 'jupyter-notebook').document;59const selection = {60start: 1,61end: 2,62isEmpty: false,63with() {64return selection;65}66};6768return {69notebook,70revealRange() { },71selections: [selection],72selection: selection,73visibleRanges: [],74viewColumn: 175};76}7778describe('Notebook Prompt Rendering', function () {79let accessor: ITestingServicesAccessor;80const contexts: IDocumentContext[] = [];81const treatmeants = {82'copilotchat.notebookPackages': false,83'copilotchat.notebookPriorities': false84};8586beforeAll(() => {87const notebookEditor = getFakeNotebookEditor();88contexts.length = 0;89contexts.push(getFakeDocumentContext(notebookEditor.notebook, 0));90contexts.push(getFakeDocumentContext(notebookEditor.notebook, 1));9192const testingServiceCollection = createExtensionUnitTestingServices();93testingServiceCollection.define(ITabsAndEditorsService, new TestingTabsAndEditorsService({94getActiveTextEditor: () => undefined,95getVisibleTextEditors: () => [],96getActiveNotebookEditor: () => notebookEditor97}));98testingServiceCollection.define(IWorkspaceService, new class extends AbstractWorkspaceService {99override fs!: vscode.FileSystem;100override textDocuments: readonly vscode.TextDocument[] = [];101override notebookDocuments: readonly vscode.NotebookDocument[] = [notebookEditor.notebook];102override onDidOpenTextDocument = Event.None;103override onDidCloseTextDocument = Event.None;104override onDidOpenNotebookDocument = Event.None;105override onDidCloseNotebookDocument = Event.None;106override onDidChangeTextDocument = Event.None;107override onDidChangeWorkspaceFolders = Event.None;108override onDidChangeNotebookDocument = Event.None;109override onDidChangeTextEditorSelection = Event.None;110override openTextDocument(uri: vscode.Uri): Promise<vscode.TextDocument> {111throw new Error('Method not implemented.');112}113override showTextDocument(document: vscode.TextDocument): Promise<void> {114throw new Error('Method not implemented.');115}116override async openNotebookDocument(uri: Uri): Promise<vscode.NotebookDocument>;117override async openNotebookDocument(notebookType: string, content?: vscode.NotebookData): Promise<vscode.NotebookDocument>;118override async openNotebookDocument(arg1: Uri | string, arg2?: vscode.NotebookData): Promise<vscode.NotebookDocument> {119throw new Error('Method not implemented.');120}121122override getWorkspaceFolders(): URI[] {123return [];124}125override getWorkspaceFolderName(workspaceFolderUri: URI): string {126return '';127}128override ensureWorkspaceIsFullyLoaded(): Promise<void> {129throw new Error('Method not implemented.');130}131override async showWorkspaceFolderPicker(): Promise<vscode.WorkspaceFolder | undefined> {132return;133}134override applyEdit(edit: vscode.WorkspaceEdit): Thenable<boolean> {135throw new Error('Method not implemented.');136}137override isResourceTrusted(_resource: vscode.Uri): Thenable<boolean> {138return Promise.resolve(true);139}140override requestResourceTrust(_options: vscode.ResourceTrustRequestOptions): Thenable<boolean | undefined> {141return Promise.resolve(true);142}143override requestWorkspaceTrust(_options?: vscode.WorkspaceTrustRequestOptions): Thenable<boolean | undefined> {144return Promise.resolve(true);145}146147});148testingServiceCollection.define(IExperimentationService, new class extends NullExperimentationService {149override getTreatmentVariable<T extends string | number | boolean>(_name: string): T | undefined {150if (_name === 'copilotchat.notebookPackages' || _name === 'copilotchat.notebookPriorities') {151return treatmeants[_name] as T;152}153154return undefined;155}156});157testingServiceCollection.define(INotebookService, new class implements INotebookService {158_serviceBrand: undefined;159async getVariables(notebook: Uri): Promise<VariablesResult[]> {160return [161{162variable: {163name: 'x',164value: '1',165type: 'int',166summary: 'int'167},168hasNamedChildren: false,169indexedChildrenCount: 0170}171];172}173async getPipPackages(notebook: Uri): Promise<PipPackage[]> {174return [175{ name: 'numpy', version: '1.0.0' }176];177}178setVariables(notebook: Uri, variables: VariablesResult[]): void {179}180getCellExecutions(notebook: vscode.Uri): vscode.NotebookCell[] {181return [];182}183runCells(notebook: Uri, range: { start: number; end: number }, autoreveal: boolean): Promise<void> {184return Promise.resolve();185}186ensureKernelSelected(notebook: Uri): Promise<void> {187return Promise.resolve();188}189populateNotebookProviders(): void {190return;191}192hasSupportedNotebooks(uri: Uri): boolean {193return false;194}195trackAgentUsage() { }196setFollowState(state: boolean): void { }197getFollowState(): boolean {198return false;199}200});201const mockLogger: ILogger = {202error: () => { /* no-op */ },203warn: () => { /* no-op */ },204info: () => { /* no-op */ },205debug: () => { /* no-op */ },206trace: () => { /* no-op */ },207show: () => { /* no-op */ },208createSubLogger(): ILogger { return mockLogger; },209withExtraTarget(): ILogger { return mockLogger; }210};211testingServiceCollection.define(IAlternativeNotebookContentService, new SimulationAlternativeNotebookContentService('json'));212testingServiceCollection.define(IAlternativeNotebookContentEditGenerator, new AlternativeNotebookContentEditGenerator(new SimulationAlternativeNotebookContentService('json'), new DiffServiceImpl(), new class implements ILogService {213_serviceBrand: undefined;214internal = mockLogger;215logger = mockLogger;216trace = mockLogger.trace;217debug = mockLogger.debug;218info = mockLogger.info;219warn = mockLogger.warn;220error = mockLogger.error;221show(preserveFocus?: boolean): void {222//223}224createSubLogger(): ILogger {225return this;226}227withExtraTarget(): ILogger {228return this;229}230}(), new NullTelemetryService()));231accessor = testingServiceCollection.createTestingAccessor();232});233234beforeEach(() => {235treatmeants['copilotchat.notebookPackages'] = false;236treatmeants['copilotchat.notebookPriorities'] = false;237});238239test('Notebook prompt structure is rendered correctly', async function () {240const endpoint = accessor.get(IInstantiationService).createInstance(MockEndpoint, undefined);241const progressReporter = { report() { } };242const renderer = PromptRenderer.create(accessor.get(IInstantiationService), endpoint, InlineChatNotebookGeneratePrompt, {243documentContext: contexts[1],244promptContext: {245query: 'print hello world',246chatVariables: new ChatVariablesCollection([]),247history: [],248}249});250const promptResult = await renderer.render(progressReporter, CancellationToken.None);251expect(promptResult.messages.length).toBe(5);252expect(getTextPart(promptResult.messages[0].content)).contains('AI programming'); // System message253expect(getTextPart(promptResult.messages[1].content)).contains('I am working on a Jupyter notebook'); // Notebook Document Context254expect(getTextPart(promptResult.messages[2].content)).contains('Now I edit a cell'); // Current Cell255expect(getTextPart(promptResult.messages[3].content)).contains('The following variables'); // Variables256expect(getTextPart(promptResult.messages[4].content)).contains('print hello world'); // User Query257});258259test('Disable package should not render packages', async function () {260treatmeants['copilotchat.notebookPackages'] = true;261const endpoint = accessor.get(IInstantiationService).createInstance(MockEndpoint, undefined);262const progressReporter = { report() { } };263const renderer = PromptRenderer.create(accessor.get(IInstantiationService), endpoint, InlineChatNotebookGeneratePrompt, {264documentContext: contexts[1],265promptContext: {266query: 'print hello world',267chatVariables: new ChatVariablesCollection([]),268history: [],269}270});271const promptResult = await renderer.render(progressReporter, CancellationToken.None);272/**273* System+Instructions274* Notebook Document Context275* Current Cell276* Variables277* User Query278*/279expect(promptResult.messages.length).toBe(5);280});281282test('Priorities: Package should be dropped first', async function () {283treatmeants['copilotchat.notebookPriorities'] = true;284const endpoint: IChatEndpoint = {285modelMaxPromptTokens: 880,286supportsToolCalls: false,287supportsVision: false,288supportsPrediction: false,289isPremium: false,290multiplier: 0,291maxOutputTokens: 4096,292tokenizer: TokenizerType.O200K,293modelProvider: 'Test',294name: 'Test',295family: 'Test',296version: 'Test',297showInModelPicker: false,298isFallback: false,299urlOrRequestMetadata: '',300model: CHAT_MODEL.GPT41,301acquireTokenizer() {302return accessor.get(ITokenizerProvider).acquireTokenizer({ tokenizer: TokenizerType.O200K });303},304processResponseFromChatEndpoint: async () => { throw new Error('Method not implemented.'); },305cloneWithTokenOverride: () => endpoint,306createRequestBody: () => { return {}; },307makeChatRequest2: () => { throw new Error('Method not implemented.'); },308makeChatRequest: async () => { throw new Error('Method not implemented.'); },309};310const progressReporter = { report() { } };311const renderer = PromptRenderer.create(accessor.get(IInstantiationService), endpoint, InlineChatNotebookGeneratePrompt, {312documentContext: contexts[1],313promptContext: {314query: 'print hello world',315chatVariables: new ChatVariablesCollection([]),316history: [],317}318});319const promptResult = await renderer.render(progressReporter, CancellationToken.None);320expect(promptResult.messages.length).toBe(5);321expect(getTextPart(promptResult.messages[3].content)).contains('The following variables'); // Variables322expect(getTextPart(promptResult.messages[4].content)).contains('print hello world'); // User Query323});324});325326327