From 89fbc31bff9f22a91d225c90b16511676d79bfe8 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 23 Jul 2024 11:07:40 +0200 Subject: [PATCH 01/45] feat: copilot new api VSCODE-550 (#757) * feat: convert copilot POC to feature utilising latest API VSCODE-550 * refactor: use prompts in code * fix: use uri path * refactor: pass participant to playground controller --- package.json | 33 ++ src/commands/index.ts | 4 + src/editors/playgroundController.ts | 26 ++ src/mdbExtensionController.ts | 42 +++ src/participant/participant.ts | 332 ++++++++++++++++++ src/templates/playgroundBasicTextTemplate.ts | 16 + .../editors/playgroundController.test.ts | 9 +- ...aygroundSelectedCodeActionProvider.test.ts | 27 ++ .../language/languageServerController.test.ts | 6 + 9 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 src/participant/participant.ts create mode 100644 src/templates/playgroundBasicTextTemplate.ts diff --git a/package.json b/package.json index dd40df552..f60d77464 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,9 @@ "color": "#3D4F58", "theme": "dark" }, + "enabledApiProposals": [ + "chatVariableResolver" + ], "license": "SEE LICENSE IN LICENSE.txt", "main": "./dist/extension.js", "scripts": { @@ -71,11 +74,25 @@ }, "activationEvents": [ "onView:mongoDB", + "onStartupFinished", "onLanguage:json", "onLanguage:javascript", "onLanguage:plaintext" ], "contributes": { + "chatParticipants": [ + { + "id": "mongodb.participant", + "name": "MongoDB", + "description": "Ask anything about MongoDB, from writing queries to questions about your cluster.", + "commands": [ + { + "name": "query", + "description": "Ask how to write MongoDB queries or pipelines. For example, you can ask: \"Show me all the documents where the address contains the word street\"." + } + ] + } + ], "viewsContainers": { "activitybar": [ { @@ -142,6 +159,14 @@ } ], "commands": [ + { + "command": "mdb.runParticipantQuery", + "title": "Run Content Generated by the Chat Participant" + }, + { + "command": "mdb.openParticipantQueryInPlayground", + "title": "Open Generated by the Chat Participant Content In Playground" + }, { "command": "mdb.connect", "title": "MongoDB: Connect" @@ -688,6 +713,14 @@ } ], "commandPalette": [ + { + "command": "mdb.openParticipantQueryInPlayground", + "when": "false" + }, + { + "command": "mdb.runParticipantQuery", + "when": "false" + }, { "command": "mdb.disconnect", "when": "mdb.connectedToMongoDB == true" diff --git a/src/commands/index.ts b/src/commands/index.ts index ebef4dec1..61513dc2c 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -73,6 +73,10 @@ enum EXTENSION_COMMANDS { MDB_START_STREAM_PROCESSOR = 'mdb.startStreamProcessor', MDB_STOP_STREAM_PROCESSOR = 'mdb.stopStreamProcessor', MDB_DROP_STREAM_PROCESSOR = 'mdb.dropStreamProcessor', + + // Chat participant. + OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND = 'mdb.openParticipantQueryInPlayground', + RUN_PARTICIPANT_QUERY = 'mdb.runParticipantQuery', } export default EXTENSION_COMMANDS; diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 64b2594a3..187f320fa 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -16,6 +16,7 @@ import { DatabaseTreeItem } from '../explorer'; import type ExportToLanguageCodeLensProvider from './exportToLanguageCodeLensProvider'; import formatError from '../utils/formatError'; import type { LanguageServerController } from '../language'; +import playgroundBasicTextTemplate from '../templates/playgroundBasicTextTemplate'; import playgroundCreateIndexTemplate from '../templates/playgroundCreateIndexTemplate'; import playgroundCreateCollectionTemplate from '../templates/playgroundCreateCollectionTemplate'; import playgroundCloneDocumentTemplate from '../templates/playgroundCloneDocumentTemplate'; @@ -44,6 +45,7 @@ import { isPlayground, getPlaygroundExtensionForTelemetry, } from '../utils/playground'; +import type { ParticipantController } from '../participant/participant'; const log = createLogger('playground controller'); @@ -132,6 +134,7 @@ export default class PlaygroundController { private _playgroundResultTextDocument?: vscode.TextDocument; private _statusView: StatusView; private _playgroundResultViewProvider: PlaygroundResultProvider; + private _participantController: ParticipantController; private _codeToEvaluate = ''; @@ -144,6 +147,7 @@ export default class PlaygroundController { activeConnectionCodeLensProvider, exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider, + participantController, }: { connectionController: ConnectionController; languageServerController: LanguageServerController; @@ -153,6 +157,7 @@ export default class PlaygroundController { activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider; exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; + participantController: ParticipantController; }) { this._connectionController = connectionController; this._activeTextEditor = vscode.window.activeTextEditor; @@ -164,6 +169,7 @@ export default class PlaygroundController { this._exportToLanguageCodeLensProvider = exportToLanguageCodeLensProvider; this._playgroundSelectedCodeActionProvider = playgroundSelectedCodeActionProvider; + this._participantController = participantController; this._connectionController.addEventListener( DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED, @@ -382,6 +388,21 @@ export default class PlaygroundController { return this._createPlaygroundFileWithContent(content); } + createPlaygroundFromParticipantQuery({ + text, + }: { + text: string; + }): Promise { + const useDefaultTemplate = !!vscode.workspace + .getConfiguration('mdb') + .get('useDefaultTemplateForPlayground'); + const content = useDefaultTemplate + ? playgroundBasicTextTemplate.replace('PLAYGROUND_CONTENT', text) + : text; + this._telemetryService.trackPlaygroundCreated('agent'); + return this._createPlaygroundFileWithContent(content); + } + createPlaygroundForCloneDocument( documentContents: string, databaseName: string, @@ -802,6 +823,11 @@ export default class PlaygroundController { return { namespace, expression }; } + async evaluateParticipantQuery({ text }: { text: string }): Promise { + this._codeToEvaluate = text; + return this._evaluatePlayground(); + } + async _transpile(): Promise { const { selectedText, importStatements, driverSyntax, builders, language } = this._exportToLanguageCodeLensProvider._exportToLanguageAddons; diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 791d85287..579757dac 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -40,6 +40,7 @@ import WebviewController from './views/webviewController'; import { createIdFactory, generateId } from './utils/objectIdHelper'; import { ConnectionStorage } from './storage/connectionStorage'; import type StreamProcessorTreeItem from './explorer/streamProcessorTreeItem'; +import { ParticipantController } from './participant/participant'; // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. @@ -63,6 +64,7 @@ export default class MDBExtensionController implements vscode.Disposable { _activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider; _editDocumentCodeLensProvider: EditDocumentCodeLensProvider; _exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; + _participantController: ParticipantController; constructor( context: vscode.ExtensionContext, @@ -105,6 +107,9 @@ export default class MDBExtensionController implements vscode.Disposable { new PlaygroundSelectedCodeActionProvider(); this._playgroundDiagnosticsCodeActionProvider = new PlaygroundDiagnosticsCodeActionProvider(); + this._participantController = new ParticipantController({ + connectionController: this._connectionController, + }); this._playgroundController = new PlaygroundController({ connectionController: this._connectionController, languageServerController: this._languageServerController, @@ -115,6 +120,7 @@ export default class MDBExtensionController implements vscode.Disposable { exportToLanguageCodeLensProvider: this._exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: this._playgroundSelectedCodeActionProvider, + participantController: this._participantController, }); this._editorsController = new EditorsController({ context, @@ -265,6 +271,42 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerEditorCommands(); this.registerTreeViewCommands(); + + // ------ CHAT PARTICIPANT ------ // + this.registerParticipantCommand( + EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + () => { + return this._playgroundController.createPlaygroundFromParticipantQuery({ + text: + this._participantController._chatResult.metadata.queryContent || '', + }); + } + ); + this.registerParticipantCommand( + EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, + () => { + return this._playgroundController.evaluateParticipantQuery({ + text: + this._participantController._chatResult.metadata.queryContent || '', + }); + } + ); + }; + + registerParticipantCommand = ( + command: string, + commandHandler: (...args: any[]) => Promise + ): void => { + const commandHandlerWithTelemetry = (args: any[]): Promise => { + this._telemetryService.trackCommandRun(command); + + return commandHandler(args); + }; + + this._context.subscriptions.push( + this._participantController.getParticipant(this._context), + vscode.commands.registerCommand(command, commandHandlerWithTelemetry) + ); }; registerCommand = ( diff --git a/src/participant/participant.ts b/src/participant/participant.ts new file mode 100644 index 000000000..d49e58cf0 --- /dev/null +++ b/src/participant/participant.ts @@ -0,0 +1,332 @@ +import * as vscode from 'vscode'; + +import { createLogger } from '../logging'; +import type ConnectionController from '../connectionController'; +import EXTENSION_COMMANDS from '../commands'; + +const log = createLogger('participant'); + +interface ChatResult extends vscode.ChatResult { + metadata: { + command: string; + databaseName?: string; + collectionName?: string; + queryContent?: string; + description?: string; + }; + stream?: vscode.ChatResponseStream; +} + +export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; +export const CHAT_PARTICIPANT_MODEL = 'gpt-4'; + +function handleEmptyQueryRequest(participantId?: string): ChatResult { + log.info('Chat request participant id', participantId); + + return { + metadata: { + command: '', + }, + errorDetails: { + message: + 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".', + }, + }; +} + +export function getRunnableContentFromString(responseContent: string) { + const matchedJSQueryContent = responseContent.match( + /```javascript((.|\n)*)```/ + ); + log.info('matchedJSQueryContent', matchedJSQueryContent); + + const queryContent = + matchedJSQueryContent && matchedJSQueryContent.length > 1 + ? matchedJSQueryContent[1] + : ''; + log.info('queryContent', queryContent); + return queryContent; +} + +export class ParticipantController { + _participant?: vscode.ChatParticipant; + _chatResult: ChatResult; + _connectionController: ConnectionController; + + constructor({ + connectionController, + }: { + connectionController: ConnectionController; + }) { + this._chatResult = { metadata: { command: '' } }; + this._connectionController = connectionController; + } + + createParticipant(context: vscode.ExtensionContext) { + // Chat participants appear as top-level options in the chat input + // when you type `@`, and can contribute sub-commands in the chat input + // that appear when you type `/`. + this._participant = vscode.chat.createChatParticipant( + CHAT_PARTICIPANT_ID, + this.chatHandler.bind(this) + ); + this._participant.iconPath = vscode.Uri.joinPath( + vscode.Uri.parse(context.extensionPath), + 'images', + 'mongodb.png' + ); + return this._participant; + } + + getParticipant(context: vscode.ExtensionContext) { + return this._participant || this.createParticipant(context); + } + + handleError(err: any, stream: vscode.ChatResponseStream): void { + // Making the chat request might fail because + // - model does not exist + // - user consent not given + // - quote limits exceeded + if (err instanceof vscode.LanguageModelError) { + log.error(err.message, err.code, err.cause); + if ( + err.cause instanceof Error && + err.cause.message.includes('off_topic') + ) { + stream.markdown( + vscode.l10n.t( + "I'm sorry, I can only explain computer science concepts.\n\n" + ) + ); + } + } + } + + async getChatResponseContent({ + messages, + stream, + token, + }: { + messages: vscode.LanguageModelChatMessage[]; + stream: vscode.ChatResponseStream; + token: vscode.CancellationToken; + }): Promise { + let responseContent = ''; + try { + const [model] = await vscode.lm.selectChatModels({ + vendor: 'copilot', + family: CHAT_PARTICIPANT_MODEL, + }); + if (model) { + const chatResponse = await model.sendRequest(messages, {}, token); + for await (const fragment of chatResponse.text) { + responseContent += fragment; + stream.markdown(fragment); + } + stream.markdown('\n\n'); + } + } catch (err) { + this.handleError(err, stream); + } + + return responseContent; + } + + // @MongoDB what is mongodb? + async handleGenericRequest({ + request, + context, + stream, + token, + }: { + request: vscode.ChatRequest; + context: vscode.ChatContext; + stream: vscode.ChatResponseStream; + token: vscode.CancellationToken; + }) { + const messages = [ + // eslint-disable-next-line new-cap + vscode.LanguageModelChatMessage.Assistant(`You are a MongoDB expert! + You create MongoDB queries and aggregation pipelines, + and you are very good at it. The user will provide the basis for the query. + Keep your response concise. Respond with markdown, code snippets are possible with '''javascript. + You can imagine the schema, collection, and database name. + Respond in MongoDB shell syntax using the '''javascript code style.'.`), + ]; + + context.history.map((historyItem) => { + if ( + historyItem.participant === CHAT_PARTICIPANT_ID && + historyItem instanceof vscode.ChatRequestTurn + ) { + // eslint-disable-next-line new-cap + messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt)); + } + + if ( + historyItem.participant === CHAT_PARTICIPANT_ID && + historyItem instanceof vscode.ChatResponseTurn + ) { + let res = ''; + for (const fragment of historyItem.response) { + res += fragment; + } + // eslint-disable-next-line new-cap + messages.push(vscode.LanguageModelChatMessage.Assistant(res)); + } + }); + + // eslint-disable-next-line new-cap + messages.push(vscode.LanguageModelChatMessage.User(request.prompt)); + + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); + + const responseContent = await this.getChatResponseContent({ + messages, + stream, + token, + }); + const queryContent = getRunnableContentFromString(responseContent); + + if (!queryContent || queryContent.trim().length === 0) { + return { metadata: { command: '' } }; + } + + stream.button({ + command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, + title: vscode.l10n.t('▶️ Run'), + }); + + stream.button({ + command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + title: vscode.l10n.t('Open in playground'), + }); + + return { + metadata: { + command: '', + stream, + queryContent, + }, + }; + } + + // @MongoDB /query find all documents where the "address" has the word Broadway in it. + async handleQueryRequest({ + request, + stream, + token, + }: { + request: vscode.ChatRequest; + context?: vscode.ChatContext; + stream: vscode.ChatResponseStream; + token: vscode.CancellationToken; + }) { + if (!request.prompt || request.prompt.trim().length === 0) { + return handleEmptyQueryRequest(this._participant?.id); + } + + let dataService = this._connectionController.getActiveDataService(); + if (!dataService) { + stream.markdown( + "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against.\n\n" + ); + // We add a delay so the user can read the message. + // TODO: maybe there is better way to handle this. + // stream.button() does not awaits so we can't use it here. + // Followups do not support input so we can't use that either. + await new Promise((resolve) => setTimeout(resolve, 1000)); + const successfullyConnected = + await this._connectionController.changeActiveConnection(); + dataService = this._connectionController.getActiveDataService(); + + if (!dataService || !successfullyConnected) { + stream.markdown( + 'No connection for command provided. Please use a valid connection for running commands.\n\n' + ); + return { metadata: { command: '' } }; + } + + stream.markdown( + `Connected to "${this._connectionController.getActiveConnectionName()}".\n\n` + ); + } + + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); + + const messages = [ + // eslint-disable-next-line new-cap + vscode.LanguageModelChatMessage.Assistant(`You are a MongoDB expert! + You create MongoDB queries and aggregation pipelines, + and you are very good at it. The user will provide the basis for the query. + Keep your response concise. Respond with markdown, code snippets are possible with '''javascript. + You can imagine the schema, collection, and database name. + Respond in MongoDB shell syntax using the '''javascript code style.`), + // eslint-disable-next-line new-cap + vscode.LanguageModelChatMessage.User(request.prompt), + ]; + const responseContent = await this.getChatResponseContent({ + messages, + stream, + token, + }); + const queryContent = getRunnableContentFromString(responseContent); + + if (!queryContent || queryContent.trim().length === 0) { + return { metadata: { command: '' } }; + } + + stream.button({ + command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, + title: vscode.l10n.t('▶️ Run'), + }); + stream.button({ + command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + title: vscode.l10n.t('Open in playground'), + }); + + return { + metadata: { + command: '', + stream, + queryContent, + }, + }; + } + + async chatHandler( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ): Promise { + if (request.command === 'query') { + this._chatResult = await this.handleQueryRequest({ + request, + context, + stream, + token, + }); + return this._chatResult; + } else if (request.command === 'docs') { + // TODO: Implement this. + } else if (request.command === 'schema') { + // TODO: Implement this. + } else if (request.command === 'logs') { + // TODO: Implement this. + } + + return await this.handleGenericRequest({ + request, + context, + stream, + token, + }); + } +} diff --git a/src/templates/playgroundBasicTextTemplate.ts b/src/templates/playgroundBasicTextTemplate.ts new file mode 100644 index 000000000..2fe3e8186 --- /dev/null +++ b/src/templates/playgroundBasicTextTemplate.ts @@ -0,0 +1,16 @@ +const template = `/* global use, db */ +// MongoDB Playground +// To disable this template go to Settings | MongoDB | Use Default Template For Playground. +// Make sure you are connected to enable completions and to be able to run a playground. +// Use Ctrl+Space inside a snippet or a string literal to trigger completions. +// The result of the last command run in a playground is shown on the results panel. +// By default the first 20 documents will be returned with a cursor. +// Use 'console.log()' to print to the debug output. +// For more documentation on playgrounds please refer to +// https://www.mongodb.com/docs/mongodb-vscode/playgrounds/ +// Select the database to use. +use('CURRENT_DATABASE'); +PLAYGROUND_CONTENT +`; + +export default template; diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index 14df23617..66af1b06b 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -22,6 +22,7 @@ import { StorageController } from '../../../storage'; import TelemetryService from '../../../telemetry/telemetryService'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { ExtensionContextStub, LanguageServerControllerStub } from '../stubs'; +import { ParticipantController } from '../../../participant/participant'; const expect = chai.expect; @@ -57,6 +58,7 @@ suite('Playground Controller Test Suite', function () { let testPlaygroundController: PlaygroundController; let showErrorMessageStub: SinonStub; let sandbox: sinon.SinonSandbox; + let testParticipantController: ParticipantController; beforeEach(() => { sandbox = sinon.createSandbox(); @@ -84,11 +86,13 @@ suite('Playground Controller Test Suite', function () { testExportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); testCodeActionProvider = new PlaygroundSelectedCodeActionProvider(); - languageServerControllerStub = new LanguageServerControllerStub( extensionContextStub, testStorageController ); + testParticipantController = new ParticipantController({ + connectionController: testConnectionController, + }); testPlaygroundController = new PlaygroundController({ connectionController: testConnectionController, languageServerController: languageServerControllerStub, @@ -98,6 +102,7 @@ suite('Playground Controller Test Suite', function () { activeConnectionCodeLensProvider: testActiveDBCodeLensProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, + participantController: testParticipantController, }); showErrorMessageStub = sandbox.stub(vscode.window, 'showErrorMessage'); sandbox.stub(testTelemetryService, 'trackNewConnection'); @@ -354,6 +359,7 @@ suite('Playground Controller Test Suite', function () { exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, + participantController: testParticipantController, }); expect(playgroundController._activeTextEditor).to.deep.equal( @@ -372,6 +378,7 @@ suite('Playground Controller Test Suite', function () { exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, + participantController: testParticipantController, }); const textFromEditor = 'var x = { name: qwerty }'; const selection = { diff --git a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts index 1dea18fbd..74f143249 100644 --- a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts +++ b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts @@ -13,6 +13,11 @@ import type { PlaygroundResult } from '../../../types/playgroundType'; import { ExportToLanguageMode } from '../../../types/playgroundType'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { ExtensionContextStub } from '../stubs'; +import { ParticipantController } from '../../../participant/participant'; +import ConnectionController from '../../../connectionController'; +import StatusView from '../../../views/statusView'; +import StorageController from '../../../storage/storageController'; +import TelemetryService from '../../../telemetry/telemetryService'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require('../../../../package.json'); @@ -33,8 +38,29 @@ suite('Playground Selected CodeAction Provider Test Suite', function () { suite('the MongoDB playground in JS', () => { const testCodeActionProvider = new PlaygroundSelectedCodeActionProvider(); const sandbox = sinon.createSandbox(); + let testStorageController: StorageController; + let testTelemetryService: TelemetryService; + let testStatusView: StatusView; + let testConnectionController: ConnectionController; + let testParticipantController: ParticipantController; beforeEach(async () => { + testStorageController = new StorageController(extensionContextStub); + testTelemetryService = new TelemetryService( + testStorageController, + extensionContextStub + ); + testStatusView = new StatusView(extensionContextStub); + testConnectionController = new ConnectionController({ + statusView: testStatusView, + storageController: testStorageController, + telemetryService: testTelemetryService, + }); + + testParticipantController = new ParticipantController({ + connectionController: testConnectionController, + }); + sandbox.replace( mdbTestExtension.testExtensionController, '_languageServerController', @@ -73,6 +99,7 @@ suite('Playground Selected CodeAction Provider Test Suite', function () { exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, + participantController: testParticipantController, }); const fakeOpenPlaygroundResult = sandbox.fake(); diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index b34944301..52a660a37 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -22,6 +22,7 @@ import { StorageController } from '../../../storage'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import TelemetryService from '../../../telemetry/telemetryService'; import { ExtensionContextStub } from '../stubs'; +import { ParticipantController } from '../../../participant/participant'; const expect = chai.expect; @@ -60,6 +61,7 @@ suite('Language Server Controller Test Suite', () => { let languageServerControllerStub: LanguageServerController; let testPlaygroundController: PlaygroundController; + let testParticipantController: ParticipantController; const sandbox = sinon.createSandbox(); @@ -67,6 +69,9 @@ suite('Language Server Controller Test Suite', () => { languageServerControllerStub = new LanguageServerController( extensionContextStub ); + testParticipantController = new ParticipantController({ + connectionController: testConnectionController, + }); testPlaygroundController = new PlaygroundController({ connectionController: testConnectionController, languageServerController: languageServerControllerStub, @@ -76,6 +81,7 @@ suite('Language Server Controller Test Suite', () => { activeConnectionCodeLensProvider: testActiveDBCodeLensProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, + participantController: testParticipantController, }); await languageServerControllerStub.startLanguageServer(); await testPlaygroundController._activeConnectionChanged(); From feaaa7c89d8d747b43b30844f702479bdb3d0d71 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 12 Aug 2024 13:03:22 +0200 Subject: [PATCH 02/45] chore: sync with main (#773) * fix: remove changelog from the bundle VSCODE-551 (#758) * Update changelog (#759) Co-authored-by: mcasimir <334881+mcasimir@users.noreply.github.com> * Update changelog (#761) Co-authored-by: alenakhineika <16307679+alenakhineika@users.noreply.github.com> * feat(telemetry): report host_id for atlas COMPASS-8092 (#763) * feat(telemetry): report host_id for atlas in VSCode COMPASS-8092 * test: add atlas host test and clean up * feat(telemetry): update new connection telemetry VSCODE-560 (#764) * feat(telemetry): update new connection telemetry VSCODE-560 * refactor: remove server_os * refactor: rename to atlas_hostname * docs: add comment * refactor(telemetry): use resolved srv from topology VSCODE-563 (#765) * Update changelog (#766) Co-authored-by: alenakhineika <16307679+alenakhineika@users.noreply.github.com> * feat(telemetry): handle IPv6 addresses when parsing hostnames VSCODE-585 (#769) * feat(telemetry): handle IPv6 addresses when parsing hostnames VSCODE-585 * test: fallback to original uri * chore: reduce bundle size and add modules to externals * chore(deps): remove unused auto-prefixer (#771) * chore: use Node.js 20 in CI VSCODE-586 (#770) * chore: use Node.js 20 in CI VSCODE-586 * fix: try to resolve a regression of out of memory crashes of webpack --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: mcasimir <334881+mcasimir@users.noreply.github.com> Co-authored-by: Rhys --- .depcheckrc | 2 - .../actions/test-and-build/action.yaml | 2 +- .github/workflows/draft-release.yaml | 2 +- .github/workflows/publish-release.yaml | 2 +- .github/workflows/test-and-build.yaml | 2 +- .vscodeignore | 1 + CHANGELOG.md | 47 +- babel.config.json | 10 + package-lock.json | 3317 +++++++++-------- package.json | 55 +- src/language/visitor.ts | 138 +- src/telemetry/connectionTelemetry.ts | 160 +- .../telemetry/connectionTelemetry.test.ts | 595 ++- webpack.config.js | 40 +- 14 files changed, 2631 insertions(+), 1742 deletions(-) create mode 100644 babel.config.json diff --git a/.depcheckrc b/.depcheckrc index 238406eb2..a1f296e4e 100644 --- a/.depcheckrc +++ b/.depcheckrc @@ -1,6 +1,4 @@ ignores: - - "@babel/core" - - "@babel/preset-typescript" - "@mongodb-js/prettier-config-devtools" - "@types/jest" - "buffer" diff --git a/.github/workflows/actions/test-and-build/action.yaml b/.github/workflows/actions/test-and-build/action.yaml index f69e43675..2b9438281 100644 --- a/.github/workflows/actions/test-and-build/action.yaml +++ b/.github/workflows/actions/test-and-build/action.yaml @@ -76,7 +76,7 @@ runs: - name: Build .vsix env: - NODE_OPTIONS: "--require ./scripts/no-npm-list-fail.js" + NODE_OPTIONS: "--require ./scripts/no-npm-list-fail.js --max_old_space_size=4096" # NOTE: --githubBranch is "The GitHub branch used to infer relative links in README.md." run: | npx vsce package --githubBranch main diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index d7e3ea565..1d4f8b5c8 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -32,7 +32,7 @@ jobs: - name: Setup Node.js Environment uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 20.16.0 - name: Determine Next Version shell: bash diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index 051b5f6e2..787c02f3c 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -12,7 +12,7 @@ jobs: - name: Setup Node.js Environment uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 20.16.0 - name: Install npm run: npm install -g npm@8.19.4 diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index e7fd88c8c..a0e1b8c9f 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -30,7 +30,7 @@ jobs: - name: Setup Node.js Environment uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 20.16.0 - name: Run tests and build uses: ./.github/workflows/actions/test-and-build diff --git a/.vscodeignore b/.vscodeignore index edb276427..1d62e6ae0 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -18,3 +18,4 @@ webpack.test.config.js playgrounds/** resources .sbom/** +CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index cc003b029..597a75947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Change Log +## [v1.7.0](https://github.com/mongodb-js/vscode/releases/tag/v1.7.0) - 2024-08-01 + +## What's Changed +* feat(telemetry): update connection telemetry VSCODE-560 by @alenakhineika in https://github.com/mongodb-js/vscode/pull/764 +* refactor(telemetry): use resolved srv from topology VSCODE-563 by @alenakhineika in https://github.com/mongodb-js/vscode/pull/765 + +**Full Changelog**: https://github.com/mongodb-js/vscode/compare/v1.6.1...v1.7.0 + + +## [v1.6.1](https://github.com/mongodb-js/vscode/releases/tag/v1.6.1) - 2024-07-15 + +## What's Changed +* fix: disconnect extension when server is closed VSCODE-536 by @alenakhineika in https://github.com/mongodb-js/vscode/pull/734 +* chore(deps): bump mongosh VSCODE-548 by @mabaasit in https://github.com/mongodb-js/vscode/pull/752 +* fix: remove changelog from the bundle VSCODE-551 by @alenakhineika in https://github.com/mongodb-js/vscode/pull/758 + +**Full Changelog**: https://github.com/mongodb-js/vscode/compare/v1.6.0...v1.6.1 + + ## [v1.6.0](https://github.com/mongodb-js/vscode/releases/tag/v1.6.0) - 2024-04-23 ## What's Changed @@ -427,31 +446,3 @@ To dig deeper please feel free to follow the links mentioned below: - Added a tooltip to fields in a collection's schema to show types found in the sampling for that field [#179](https://github.com/mongodb-js/vscode/pull/179) -## [v0.2.0](https://github.com/mongodb-js/vscode/releases/tag/v0.2.0) - 2020-10-01 - -### Added -- Added a Playgrounds panel that displays `.mongodb` playground files in the current VSCode workspace -- Added a setting to configure which folders and files are excluded from the playgrounds panel file searching -- Added a help and resources panel to the explorer with links to documentation and feedback portals -- Added a button to the indexes folder in the tree view which creates a playground prefilled with an index creation script -### Changed -- Updated our mongosh dependency to 0.4.2 to bring more functionality to playgrounds -### Fixed -- Fixed indexes expanded state caching in the connection explorer panel tree view - - -## [v0.1.1](https://github.com/mongodb-js/vscode/releases/tag/v0.1.1) - 2020-08-10 - -### Added - -- Added a search for documents playground shortcut in the tree explorer view -- Added a copy field name right click action in a collection's schema in the tree explorer view -- Added a document count for a collection in the tree view (with hover tooltip for the full count) -- Added the ability to change the current connection when in an open playground file by using the codelens on the first line of the playground - -### Changed - -- Allow connecting to a new connection while already connecting to another connection -- Allow removing a new connection while it is connecting - - diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 000000000..b555d01bc --- /dev/null +++ b/babel.config.json @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@babel/preset-typescript", + { + "rewriteImportExtensions": true + } + ] + ] +} diff --git a/package-lock.json b/package-lock.json index 78d723169..77fc66e0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,20 +15,20 @@ "@mongodb-js/connection-form": "^1.22.2", "@mongodb-js/connection-info": "^0.1.2", "@mongodb-js/mongodb-constants": "^0.10.2", - "@mongosh/browser-runtime-electron": "^2.2.10", - "@mongosh/i18n": "^2.2.10", - "@mongosh/service-provider-server": "^2.2.10", - "@mongosh/shell-api": "^2.2.10", + "@mongosh/browser-runtime-electron": "^2.2.15", + "@mongosh/i18n": "^2.2.15", + "@mongosh/service-provider-server": "^2.2.15", + "@mongosh/shell-api": "^2.2.15", "@segment/analytics-node": "^1.3.0", - "bson": "^6.7.0", + "bson": "^6.8.0", "bson-transpilers": "^2.2.0", - "debug": "^4.3.5", + "debug": "^4.3.6", "dotenv": "^16.4.5", "lodash": "^4.17.21", "micromatch": "^4.0.7", - "mongodb": "^6.7.0", + "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", - "mongodb-cloud-info": "^2.1.2", + "mongodb-cloud-info": "^2.1.4", "mongodb-connection-string-url": "^3.0.1", "mongodb-data-service": "^22.21.1", "mongodb-log-writer": "^1.4.2", @@ -37,29 +37,28 @@ "numeral": "^2.0.6", "react": "^17.0.2", "react-dom": "^17.0.2", - "resolve-mongodb-srv": "^1.1.5", "ts-log": "^2.2.5", "uuid": "^8.3.2", "vscode-languageclient": "^8.1.0", "vscode-languageserver": "^8.1.0", - "vscode-languageserver-textdocument": "^1.0.11" + "vscode-languageserver-textdocument": "^1.0.12" }, "devDependencies": { "@babel/preset-typescript": "^7.24.7", + "@babel/types": "^7.25.2", "@mongodb-js/oidc-mock-provider": "^0.9.1", "@mongodb-js/oidc-plugin": "^0.4.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", - "@mongodb-js/sbom-tools": "^0.7.0", - "@mongodb-js/signing-utils": "^0.3.4", - "@mongosh/service-provider-core": "^2.2.10", + "@mongodb-js/sbom-tools": "^0.7.1", + "@mongodb-js/signing-utils": "^0.3.5", + "@mongosh/service-provider-core": "^2.2.15", "@testing-library/react": "^12.1.5", - "@types/babel__core": "^7.20.5", "@types/babel__traverse": "^7.20.6", - "@types/chai": "^4.3.16", + "@types/chai": "^4.3.17", "@types/debug": "^4.1.12", "@types/glob": "^7.2.0", "@types/jest": "^26.0.24", - "@types/micromatch": "^4.0.7", + "@types/micromatch": "^4.0.9", "@types/mkdirp": "^2.0.0", "@types/mocha": "^8.2.3", "@types/node": "^14.18.63", @@ -68,15 +67,14 @@ "@types/sinon": "^9.0.11", "@types/sinon-chai": "^3.2.12", "@types/uuid": "^8.3.4", - "@types/vscode": "^1.90.0", + "@types/vscode": "^1.92.0", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", - "@vscode/test-electron": "^2.4.0", - "@vscode/vsce": "^2.29.0", + "@vscode/test-electron": "^2.4.1", + "@vscode/vsce": "^2.31.1", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", - "autoprefixer": "^10.4.19", "buffer": "^6.0.3", - "chai": "^4.4.1", + "chai": "^4.5.0", "chai-as-promised": "^7.1.2", "cross-env": "^7.0.3", "crypto-browserify": "^3.12.0", @@ -85,18 +83,18 @@ "enzyme": "^3.11.0", "eslint": "^8.57.0", "eslint-config-mongodb-js": "^5.0.3", - "eslint-plugin-mocha": "^10.4.3", + "eslint-plugin-mocha": "^10.5.0", "fork-ts-checker-webpack-plugin": "^9.0.2", "glob": "^7.2.3", "jest": "^26.6.3", "jest-junit": "^12.3.0", "jest-transform-stub": "^2.0.0", "mkdirp": "^1.0.4", - "mocha": "^10.5.1", + "mocha": "^10.7.3", "mocha-junit-reporter": "^2.2.1", "mocha-multi": "^1.1.7", "mongodb-client-encryption": "^6.0.1", - "mongodb-runner": "^5.6.2", + "mongodb-runner": "^5.6.4", "node-fetch": "^2.7.0", "node-loader": "^0.6.0", "npm-run-all": "^4.1.5", @@ -108,20 +106,21 @@ "sinon": "^9.2.4", "sinon-chai": "^3.7.0", "stream-browserify": "^3.0.0", + "terser-webpack-plugin": "^5.3.10", "ts-jest": "^26.5.6", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "typescript": "^4.9.5", - "webpack": "^5.92.1", + "webpack": "^5.93.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.10.0", "xvfb-maybe": "^0.2.1" }, "engines": { - "node": ">=16.16.0", - "npm": ">=8.19.4", - "vscode": "^1.90.2" + "node": ">=20.9.0", + "npm": ">=10.1.0", + "vscode": "^1.92.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1135,11 +1134,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dependencies": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.25.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -1184,19 +1183,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", - "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", + "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/traverse": "^7.25.0", "semver": "^6.3.1" }, "engines": { @@ -1215,48 +1212,14 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", - "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1275,15 +1238,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" @@ -1305,22 +1267,22 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -1354,21 +1316,10 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "engines": { "node": ">=6.9.0" } @@ -1481,9 +1432,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1669,11 +1623,11 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", - "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1683,13 +1637,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", - "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-simple-access": "^7.24.7" }, "engines": { @@ -1728,14 +1682,15 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", - "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz", + "integrity": "sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-typescript": "^7.24.7" }, "engines": { @@ -1776,31 +1731,28 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1809,11 +1761,11 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, @@ -2287,6 +2239,102 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -4046,6 +4094,120 @@ "node": ">=12" } }, + "node_modules/@mongodb-js/devtools-proxy-support": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.2.tgz", + "integrity": "sha512-qMSe/5XVEK3xXtMhtv+InIRuanH5nDdDo8yD3gFvsw5pRhI9qM5m06imfgx9X1woAFdntIUNy72lGNi2glbOaA==", + "dependencies": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "optional": true + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/system-ca": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/system-ca/-/system-ca-2.0.1.tgz", + "integrity": "sha512-9ZDV9yl8ph6Op67wDGPr4LykX86usE9x3le+XZSHfVMiiVJ5IRgmCWjLgxyz35ju9H3GDIJJZm4ogAeIfN5cQQ==", + "optionalDependencies": { + "macos-export-certificate-and-key": "^1.2.0", + "win-export-certificate-and-key": "^2.1.0" + } + }, + "node_modules/@mongodb-js/devtools-proxy-support/node_modules/win-export-certificate-and-key": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/win-export-certificate-and-key/-/win-export-certificate-and-key-2.1.0.tgz", + "integrity": "sha512-WeMLa/2uNZcS/HWGKU2G1Gzeh3vHpV/UFvwLhJLKxPHYFAbubxxVcJbqmPXaqySWK1Ymymh16zKK5WYIJ3zgzA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "win32" + ], + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^3.1.0" + } + }, "node_modules/@mongodb-js/mongodb-constants": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.10.2.tgz", @@ -4055,9 +4217,9 @@ } }, "node_modules/@mongodb-js/mongodb-downloader": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.2.tgz", - "integrity": "sha512-bhMfxzaBy31RveAu7qqON3nVXRHYmxJXyC3lZI+mK+4DhagKZdGHJpMkLmHQRt+wAxMR6ldI9YlcWjHSqceIsQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.3.tgz", + "integrity": "sha512-PIu8qF6J02lQsxfq8vdcfvueb8H5mrB43UaOl1E4AQMrOhm403imAZYKH61qe+7J7WzlVFck0zGI/7brHLHcNQ==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -4150,17 +4312,17 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", - "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", "dependencies": { "sparse-bitfield": "^3.0.3" } }, "node_modules/@mongodb-js/sbom-tools": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/sbom-tools/-/sbom-tools-0.7.0.tgz", - "integrity": "sha512-hjc5XrDMVaKdecLzl6IkXT2VO8fudF6aNQLRFlhJ528B5KEKBaahKU4cXnFLV7BRpi1E59FrLg3S7U6bYBfcaw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/sbom-tools/-/sbom-tools-0.7.1.tgz", + "integrity": "sha512-nXUmuUTbdVkbp0LrKOlpToOmAY6S2MSdpuJkhuN7m75gq/UqAmowzQEIWptKbPouaRD2KbncTnXLIDqsYPLm1w==", "dev": true, "dependencies": { "@octokit/rest": "^20.1.1", @@ -4179,9 +4341,9 @@ } }, "node_modules/@mongodb-js/signing-utils": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@mongodb-js/signing-utils/-/signing-utils-0.3.4.tgz", - "integrity": "sha512-D9rbB6HMXvBAPu2f3Wy3r2rggrlr7NlNt2hn5rjPM27Q2nA6AKL2mMZNkWCHmehuc3l9jdMWfaQgjvIuMMLMew==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/signing-utils/-/signing-utils-0.3.5.tgz", + "integrity": "sha512-59gZ2dTCtZZDloMOxGU6gEY9T6EEXGAMjvoK7o7ETRvI9mU2B95aq59eh0tSf/HtV+CCbIwsbSy2LqqT5GZqKg==", "dev": true, "dependencies": { "@types/ssh2": "^1.11.19", @@ -4189,6 +4351,17 @@ "ssh2": "^1.15.0" } }, + "node_modules/@mongodb-js/socksv5": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@mongodb-js/socksv5/-/socksv5-0.0.10.tgz", + "integrity": "sha512-JDz2fLKsjMiSNUxKrCpGptsgu7DzsXfu4gnUQ3RhUaBS1d4YbLrt6HejpckAiHIAa+niBpZAeiUsoop0IihWsw==", + "dependencies": { + "ip-address": "^9.0.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@mongodb-js/ssh-tunnel": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mongodb-js/ssh-tunnel/-/ssh-tunnel-2.2.1.tgz", @@ -4200,12 +4373,12 @@ } }, "node_modules/@mongosh/arg-parser": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/arg-parser/-/arg-parser-2.2.10.tgz", - "integrity": "sha512-QvXziAmACrFty7id5I0s3ZOetIc1wWhjtpytGMtchEiusTQOO+NJy74UMrxK+NGxKPDRL0MroRIo05/LEfPkJg==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/arg-parser/-/arg-parser-2.2.15.tgz", + "integrity": "sha512-KGYiw5bl8cv6wDSrTCDF1B2PjddPhiD5BkulXvgpkfuD5bB2zTJSgpx+EGGcD60OSDRnMdk0tu9AY8uIExtNvA==", "dependencies": { - "@mongosh/errors": "2.2.10", - "@mongosh/i18n": "2.2.10", + "@mongosh/errors": "2.2.15", + "@mongosh/i18n": "2.2.15", "mongodb-connection-string-url": "^3.0.1" }, "engines": { @@ -4213,9 +4386,9 @@ } }, "node_modules/@mongosh/async-rewriter2": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/async-rewriter2/-/async-rewriter2-2.2.10.tgz", - "integrity": "sha512-ssY+WxY/oOHPZAIjrYzUYhCr1K2towzshxQpYw3nk5JyjTdvN4gc0xnrWMNLJonEOD/ADTdlneFshQhuh/V1vQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/async-rewriter2/-/async-rewriter2-2.2.15.tgz", + "integrity": "sha512-y7LyjulLYe0QodRa4YIpvpHt23VQWrFGx4C5AD3IVVFhgNd0yxg2bWLIMaFsM7wwgbGJU3BxnVecAnHOgiuRHg==", "dependencies": { "@babel/core": "^7.22.8", "@babel/plugin-transform-destructuring": "^7.22.5", @@ -4231,12 +4404,12 @@ } }, "node_modules/@mongosh/autocomplete": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/autocomplete/-/autocomplete-2.2.10.tgz", - "integrity": "sha512-iLrZ1vfA67jKwEWtSKGYAZ7QM5beYFo4AsmiV+KCXwtcZref3dX1OgcjBvRW8dRvSonz8/7RnN9r8QtsTyw2xw==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/autocomplete/-/autocomplete-2.2.15.tgz", + "integrity": "sha512-R1rZVWLNmlOsOVGoHCdAxB0mx7J1A4ElPvzRBWcPW+PSEzlTT/9j0AT87exK/jjUE8ZnkzUw/soh4tqFQIjwAA==", "dependencies": { "@mongodb-js/mongodb-constants": "^0.10.1", - "@mongosh/shell-api": "2.2.10", + "@mongosh/shell-api": "2.2.15", "semver": "^7.5.4" }, "engines": { @@ -4244,72 +4417,72 @@ } }, "node_modules/@mongosh/browser-runtime-core": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-core/-/browser-runtime-core-2.2.10.tgz", - "integrity": "sha512-tN2TwbA/ANuZllWBQaZPYKjGS4M+F8UbPkHS7b08DeixbJB457Dh2HNDXztmADuLPht82DurRxx6nS0suNFNXQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-core/-/browser-runtime-core-2.2.15.tgz", + "integrity": "sha512-jBy6GizoPEvwDJCl53YDQY5Lv1F4ADL0DEqaFvKk0Ltav8EkvCcsmZTY6Kf9MmgVJGXTawWSxKoj5KLj2QUr4g==", "dependencies": { - "@mongosh/autocomplete": "2.2.10", - "@mongosh/service-provider-core": "2.2.10", - "@mongosh/shell-api": "2.2.10", - "@mongosh/shell-evaluator": "2.2.10" + "@mongosh/autocomplete": "2.2.15", + "@mongosh/service-provider-core": "2.2.15", + "@mongosh/shell-api": "2.2.15", + "@mongosh/shell-evaluator": "2.2.15" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/browser-runtime-electron": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-electron/-/browser-runtime-electron-2.2.10.tgz", - "integrity": "sha512-LmI/VVIrchb51vfTKcFZ/Q4xVzQuHnaU5/LdhRwBaAKaxEp02QwQ0IskOUg6MmU1MwDXFstwhn74nn6ZZeBhWg==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-electron/-/browser-runtime-electron-2.2.15.tgz", + "integrity": "sha512-bMxxjn9F6nxvu/qklOUc9jO9nuK24b5qzMZblHgJilicmFVtUPofZ6B5bEaxYoQHEHyB7RITs/MZmXpV6mDehg==", "dependencies": { - "@mongosh/browser-runtime-core": "2.2.10", - "@mongosh/service-provider-core": "2.2.10", - "@mongosh/types": "2.2.10" + "@mongosh/browser-runtime-core": "2.2.15", + "@mongosh/service-provider-core": "2.2.15", + "@mongosh/types": "2.2.15" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/errors": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/errors/-/errors-2.2.10.tgz", - "integrity": "sha512-jR8dv9MYYRwr+Yri/KI6HAuob0zdVBQOrMvjc+ygBbTIkL3wh1iOrjZKZuYUsjei1FDxLA8NywftAoHDchq2Tg==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/errors/-/errors-2.2.15.tgz", + "integrity": "sha512-RHCRv3Fg/xWS5XV4hOyh6KDBrn2kld+J5PVtXfsuke73jfQTLlR2PGMzSEpPWiayRLgLExq56qdXGOtNecmhuA==", "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/history": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/history/-/history-2.2.10.tgz", - "integrity": "sha512-EmMKvsytTXq/1tAwxZUvl+6+gCrarWdEDB9mO9vTCFneOgB0ao2jpo7KA9Jc63r8RYhDH78dtjFsSuFeox3AiA==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/history/-/history-2.2.15.tgz", + "integrity": "sha512-GV1i3RmG38+OUxBnqTeAlcPezkJ4fH3bBs4bwvLEV7iXMcVNzNoJBMyDa7gO6er45w38Kczx9kVDIOYdutt2Yg==", "dependencies": { "mongodb-connection-string-url": "^3.0.1", - "mongodb-redact": "^0.2.3" + "mongodb-redact": "^1.1.2" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/i18n": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/i18n/-/i18n-2.2.10.tgz", - "integrity": "sha512-TxyFOhdXqCN0AXGcWziPzZ+YBf8KthnZIbOVSyA+C1a0jynfMf+XBnLnNpwvKfGZUeQ1VyZkrKLPopG7e2nuIA==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/i18n/-/i18n-2.2.15.tgz", + "integrity": "sha512-7pjQbvJbtaglZKj86/2GRQnXLRekmpTPIVR2M58kAVXaNGqGrfCpe6mkBEkIwdjk6UHQIvkwMSzUIbFGm7nFvA==", "dependencies": { - "@mongosh/errors": "2.2.10" + "@mongosh/errors": "2.2.15" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/service-provider-core": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/service-provider-core/-/service-provider-core-2.2.10.tgz", - "integrity": "sha512-XB+G24WjTIYXnHoToJhDJMwMV7pCcgCwgme0MiE6lzcYs5HwlHhk9tzBwv0+i+3NcCb5SMqU38iUMuGG6jKJdQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/service-provider-core/-/service-provider-core-2.2.15.tgz", + "integrity": "sha512-Pk+Sxxf0rE7KacEMZvhGjr15cWkV+lcbI8cv5Hf7Taxj8kLXfbKM45WBIgGtMDTh/fbmbT15qI7StG5sCO8CCg==", "dependencies": { "@aws-sdk/credential-providers": "^3.525.0", - "@mongosh/errors": "2.2.10", + "@mongosh/errors": "2.2.15", "bson": "^6.7.0", - "mongodb": "^6.7.0", + "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2" }, "engines": { @@ -4320,17 +4493,17 @@ } }, "node_modules/@mongosh/service-provider-server": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/service-provider-server/-/service-provider-server-2.2.10.tgz", - "integrity": "sha512-7WxxKZgXNdW7f6vUCmqqWPexPOFqy/n155rFeyWhRVBI5mdbb/Sb5vsdqo1+6AyDpCXsLdl8CldsqmOfGKG36A==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/service-provider-server/-/service-provider-server-2.2.15.tgz", + "integrity": "sha512-TtQVsMlQhZqRQ6Aj9tUcSvNLL6tthkzx3m/FhBxyT/7rxxuK56w0IUVg31Cqq/gz9xUfkP3JiKounIgYqRCbXQ==", "dependencies": { - "@mongodb-js/devtools-connect": "^3.0.1", + "@mongodb-js/devtools-connect": "^3.0.5", "@mongodb-js/oidc-plugin": "^1.0.2", - "@mongosh/errors": "2.2.10", - "@mongosh/service-provider-core": "2.2.10", - "@mongosh/types": "2.2.10", + "@mongosh/errors": "2.2.15", + "@mongosh/service-provider-core": "2.2.15", + "@mongosh/types": "2.2.15", "aws4": "^1.12.0", - "mongodb": "^6.7.0", + "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", "socks": "^2.8.3" }, @@ -4343,32 +4516,37 @@ } }, "node_modules/@mongosh/service-provider-server/node_modules/@mongodb-js/devtools-connect": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.0.1.tgz", - "integrity": "sha512-xbk/eGHPQTQz4VSpGb5oRqSSbzipcFDODrAc4YtYFrb0980buOAopO71NozCbQoVnoiO1pYVIqcnrZMHkdaJzg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.2.tgz", + "integrity": "sha512-0d/9hiNnVxFjgu0HtbUSMOem/hMtpj7aKj/QN3UsABGJ8jBxMXzE90jNP6JOJ+Nt5dmlCX2iTMvtJiBIWOtCZA==", "dependencies": { - "@mongodb-js/oidc-http-server-pages": "1.1.1", + "@mongodb-js/devtools-proxy-support": "^0.3.2", + "@mongodb-js/oidc-http-server-pages": "1.1.2", "lodash.merge": "^4.6.2", "mongodb-connection-string-url": "^3.0.0", - "socks": "^2.7.3", - "system-ca": "^1.0.2" + "socks": "^2.7.3" }, "optionalDependencies": { "kerberos": "^2.1.0", - "mongodb-client-encryption": "^6.0.0", + "mongodb-client-encryption": "^6.0.0 || ^6.1.0-alpha.0", "os-dns-native": "^1.2.0", "resolve-mongodb-srv": "^1.1.1" }, "peerDependencies": { - "@mongodb-js/oidc-plugin": "^1.0.0", - "mongodb": "^5.8.1 || ^6.0.0", + "@mongodb-js/oidc-plugin": "^1.1.0", + "mongodb": "^6.8.0", "mongodb-log-writer": "^1.4.2" } }, + "node_modules/@mongosh/service-provider-server/node_modules/@mongodb-js/oidc-http-server-pages": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.1.2.tgz", + "integrity": "sha512-YcRlJ/HkKMahWvaPFTN/al5MGGX6CKQTsBHFTpNduxmeHd40jO8Cj5W7dfrqJAoit5E/G+GOwMaEfWdNrdlihg==" + }, "node_modules/@mongosh/service-provider-server/node_modules/@mongodb-js/oidc-plugin": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.0.2.tgz", - "integrity": "sha512-hwTbkmJ31RPB5ksA6pLepnaQOBz6iurE+uH89B1IIJdxVuiO0Qz+OqpTN8vk8LZzcVDb/WbNoxqxogCWwMqFKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", + "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", "dependencies": { "express": "^4.18.2", "open": "^9.1.0", @@ -4379,72 +4557,77 @@ } }, "node_modules/@mongosh/shell-api": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/shell-api/-/shell-api-2.2.10.tgz", - "integrity": "sha512-Cgcb0U5wqzmTmwAmMkSqhZ3fR4PjqNJ2px61i/9JzWAgIEDSbv9Xni3mcfDRQd/qjLHCEUlAPFgEBu7Cpk0qBQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/shell-api/-/shell-api-2.2.15.tgz", + "integrity": "sha512-HkJhDKWHRRqa7fznsRVp/ivolM7RKeCyTuJXMVFym3qt4wlC63Tc3IQjm8HYORlFGRz04AOOwCgzkIp8ddPXkg==", "dependencies": { - "@mongosh/arg-parser": "2.2.10", - "@mongosh/errors": "2.2.10", - "@mongosh/history": "2.2.10", - "@mongosh/i18n": "2.2.10", - "@mongosh/service-provider-core": "2.2.10", - "mongodb-redact": "^0.2.3" + "@mongosh/arg-parser": "2.2.15", + "@mongosh/errors": "2.2.15", + "@mongosh/history": "2.2.15", + "@mongosh/i18n": "2.2.15", + "@mongosh/service-provider-core": "2.2.15", + "mongodb-redact": "^1.1.2" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/shell-evaluator": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/shell-evaluator/-/shell-evaluator-2.2.10.tgz", - "integrity": "sha512-9v/p5WDu+Ur7+jhXajkBOvpINcRORA1UpdMOV4sBGQ623VbmoDd7xFeFPoi0uWzV50qW5yNRlh3+dvsG/jdKmQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/shell-evaluator/-/shell-evaluator-2.2.15.tgz", + "integrity": "sha512-Km/rThnbklPiYfNd/K1qFUNXICMRaYVq1pOWWSYbrT7a97KcFHIoD2OgUUudksuva4zc24CfeP5GSWRtYpbq+w==", "dependencies": { - "@mongosh/async-rewriter2": "2.2.10", - "@mongosh/history": "2.2.10", - "@mongosh/shell-api": "2.2.10" + "@mongosh/async-rewriter2": "2.2.15", + "@mongosh/history": "2.2.15", + "@mongosh/shell-api": "2.2.15" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/types": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/types/-/types-2.2.10.tgz", - "integrity": "sha512-BfCcmbC3givNCgYZxf6aUJy+/nHg2By6Hs9gZ/WMGgiedMuL5fRE18dGlwy3Aog7Jc6tVkBCMtOpYgjVnUPoxQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/types/-/types-2.2.15.tgz", + "integrity": "sha512-HkhZkjrkK9w+QHd2kPl7mspZUOpCUmgEvvHLMHmhpaYksLcxm2H4/H+s5F1Kj3EpuC9yyOHuvfC3ZMhDOgF0tg==", "dependencies": { - "@mongodb-js/devtools-connect": "^3.0.1" + "@mongodb-js/devtools-connect": "^3.0.5" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/types/node_modules/@mongodb-js/devtools-connect": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.0.1.tgz", - "integrity": "sha512-xbk/eGHPQTQz4VSpGb5oRqSSbzipcFDODrAc4YtYFrb0980buOAopO71NozCbQoVnoiO1pYVIqcnrZMHkdaJzg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.2.tgz", + "integrity": "sha512-0d/9hiNnVxFjgu0HtbUSMOem/hMtpj7aKj/QN3UsABGJ8jBxMXzE90jNP6JOJ+Nt5dmlCX2iTMvtJiBIWOtCZA==", "dependencies": { - "@mongodb-js/oidc-http-server-pages": "1.1.1", + "@mongodb-js/devtools-proxy-support": "^0.3.2", + "@mongodb-js/oidc-http-server-pages": "1.1.2", "lodash.merge": "^4.6.2", "mongodb-connection-string-url": "^3.0.0", - "socks": "^2.7.3", - "system-ca": "^1.0.2" + "socks": "^2.7.3" }, "optionalDependencies": { "kerberos": "^2.1.0", - "mongodb-client-encryption": "^6.0.0", + "mongodb-client-encryption": "^6.0.0 || ^6.1.0-alpha.0", "os-dns-native": "^1.2.0", "resolve-mongodb-srv": "^1.1.1" }, "peerDependencies": { - "@mongodb-js/oidc-plugin": "^1.0.0", - "mongodb": "^5.8.1 || ^6.0.0", + "@mongodb-js/oidc-plugin": "^1.1.0", + "mongodb": "^6.8.0", "mongodb-log-writer": "^1.4.2" } }, + "node_modules/@mongosh/types/node_modules/@mongodb-js/oidc-http-server-pages": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.1.2.tgz", + "integrity": "sha512-YcRlJ/HkKMahWvaPFTN/al5MGGX6CKQTsBHFTpNduxmeHd40jO8Cj5W7dfrqJAoit5E/G+GOwMaEfWdNrdlihg==" + }, "node_modules/@mongosh/types/node_modules/@mongodb-js/oidc-plugin": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.0.2.tgz", - "integrity": "sha512-hwTbkmJ31RPB5ksA6pLepnaQOBz6iurE+uH89B1IIJdxVuiO0Qz+OqpTN8vk8LZzcVDb/WbNoxqxogCWwMqFKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", + "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", "peer": true, "dependencies": { "express": "^4.18.2", @@ -4668,6 +4851,16 @@ "nv": "bin/nv" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", @@ -5609,6 +5802,11 @@ "node": ">= 6" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -5697,9 +5895,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.16", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.16.tgz", - "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", + "version": "4.3.17", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.17.tgz", + "integrity": "sha512-zmZ21EWzR71B4Sscphjief5djsLre50M6lI622OSySTmn9DB3j+C3kWroHfBQWXbOBwbgg/M8CG/hUxDLIloow==", "dev": true }, "node_modules/@types/debug": { @@ -5833,9 +6031,9 @@ } }, "node_modules/@types/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-C/FMQ8HJAZhTsDpl4wDKZdMeeW5USjgzOczUwTGbRc1ZopPgOhIEnxY2ZgUrsuyy4DwK1JVOJZKFakv3TbCKiA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz", + "integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==", "dev": true, "dependencies": { "@types/braces": "*" @@ -6018,9 +6216,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.90.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.90.0.tgz", - "integrity": "sha512-oT+ZJL7qHS9Z8bs0+WKf/kQ27qWYR3trsXpq46YDjFqBsMLG4ygGGjPaJ2tyrH0wJzjOEmDyg9PDJBBhWg9pkQ==", + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz", + "integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==", "dev": true }, "node_modules/@types/webidl-conversions": { @@ -6255,13 +6453,13 @@ "dev": true }, "node_modules/@vscode/test-electron": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.0.tgz", - "integrity": "sha512-yojuDFEjohx6Jb+x949JRNtSn6Wk2FAh4MldLE3ck9cfvCqzwxF32QsNy1T9Oe4oT+ZfFcg0uPUCajJzOmPlTA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz", + "integrity": "sha512-Gc6EdaLANdktQ1t+zozoBVRynfIsMKMc94Svu1QreOBC8y76x4tvaK32TljrLi1LI2+PK58sDVbL7ALdqf3VRQ==", "dev": true, "dependencies": { "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", + "https-proxy-agent": "^7.0.5", "jszip": "^3.10.1", "ora": "^7.0.1", "semver": "^7.6.2" @@ -6341,9 +6539,9 @@ } }, "node_modules/@vscode/test-electron/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -6465,9 +6663,9 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.29.0.tgz", - "integrity": "sha512-63+aEO8SpjE6qKiIh2Cqy/P9zC7+USElGwpEdkyPp89xIBDBr5IqeNS3zkD3mp3wZqbvHIpJsCCNu74WQirYCg==", + "version": "2.31.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.31.1.tgz", + "integrity": "sha512-LwEQFKXV21C4/brvGPH/9+7ZOUM5cbK7oJ4fVmy0YG75NIy1HV8eMSoBZrl+u23NxpAhor62Cu1aI+JFtCtjSg==", "dev": true, "dependencies": { "@azure/identity": "^4.1.0", @@ -6478,7 +6676,7 @@ "cockatiel": "^3.1.2", "commander": "^6.2.1", "form-data": "^4.0.0", - "glob": "^7.0.6", + "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", @@ -6488,7 +6686,7 @@ "parse-semver": "^1.1.1", "read": "^1.0.7", "semver": "^7.5.2", - "tmp": "^0.2.1", + "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", @@ -6652,6 +6850,15 @@ "node": ">=4" } }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@vscode/vsce/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -6699,6 +6906,44 @@ "node": ">=0.8.0" } }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vscode/vsce/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -6708,6 +6953,15 @@ "node": ">=4" } }, + "node_modules/@vscode/vsce/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@vscode/vsce/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -7159,9 +7413,9 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -7464,6 +7718,17 @@ "node": ">=0.10.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -7473,11 +7738,6 @@ "node": ">=4" } }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -7496,43 +7756,6 @@ "node": ">= 4.5.0" } }, - "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -7774,6 +7997,14 @@ } ] }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -8059,9 +8290,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -8077,10 +8308,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -8111,9 +8342,9 @@ } }, "node_modules/bson": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", - "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", "engines": { "node": ">=16.20.1" } @@ -8362,9 +8593,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001636", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", - "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "funding": [ { "type": "opencollective", @@ -8393,9 +8624,9 @@ } }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -8404,7 +8635,7 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" @@ -8422,6 +8653,15 @@ "chai": ">= 2.1.2 < 6" } }, + "node_modules/chai/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8824,14 +9064,6 @@ "node": ">=16" } }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -9239,6 +9471,14 @@ "node": ">=0.4.0" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -9260,9 +9500,9 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -9720,6 +9960,19 @@ "node": ">=0.10.0" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -9889,9 +10142,9 @@ "optional": true }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -10252,9 +10505,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.811", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.811.tgz", - "integrity": "sha512-CDyzcJ5XW78SHzsIOdn27z8J4ist8eaFLhdto2hSMSJQgsiwvbv2fbizcKUICryw1Wii1TI/FEkvzvJsR3awrA==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", + "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==" }, "node_modules/electron/node_modules/@types/node": { "version": "20.14.8", @@ -10594,7 +10847,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -10615,7 +10867,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "optional": true, "engines": { "node": ">=0.10.0" @@ -11104,9 +11355,9 @@ } }, "node_modules/eslint-plugin-mocha": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.4.3.tgz", - "integrity": "sha512-emc4TVjq5Ht0/upR+psftuz6IBG5q279p+1dSRDeHf+NS9aaerBi3lXKo1SEzwC29hFIW21gO89CEWSvRsi8IQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", "dev": true, "dependencies": { "eslint-utils": "^3.0.0", @@ -11454,7 +11705,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -11469,7 +11719,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12083,6 +12332,28 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -12272,6 +12543,34 @@ "node": ">=0.10.0" } }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", @@ -12393,6 +12692,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -12401,19 +12711,6 @@ "node": ">= 0.6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -12548,239 +12845,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gce-ips": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/gce-ips/-/gce-ips-1.0.2.tgz", - "integrity": "sha512-cg2Mxsqdk3Ja2VkYgvvPgXN0+PoS0Sx1ts3J7oLV/umOybgNSfsEsRiqNIItdv0jYPXQx/5xwziGYNZ8s6SiRA==", - "dependencies": { - "async": "^1.5.2", - "ip-range-check": "0.0.1", - "lodash.assign": "^4.0.8", - "yargs": "^4.7.0" - }, - "bin": { - "gce-ips": "bin/gce-ips.js" - } - }, - "node_modules/gce-ips/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/gce-ips/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, - "node_modules/gce-ips/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dependencies": { - "is-utf8": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gce-ips/node_modules/y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" - }, - "node_modules/gce-ips/node_modules/yargs": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", - "integrity": "sha512-LqodLrnIDM3IFT+Hf/5sxBnEGECrfdC1uIbgZeJmESCSo4HoCAaKEus8MylXHAkdacGc0ye+Qa+dpkuom8uVYA==", - "dependencies": { - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "lodash.assign": "^4.0.3", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.1", - "which-module": "^1.0.0", - "window-size": "^0.2.0", - "y18n": "^3.2.1", - "yargs-parser": "^2.4.1" - } - }, - "node_modules/gce-ips/node_modules/yargs-parser": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", - "integrity": "sha512-9pIKIJhnI5tonzG6OnCFlz/yln8xHYcGl+pn3xR0Vzff0vzN1PbNRaelgfgRUwZ3s4i3jvxT9WhmUGL4whnasA==", - "dependencies": { - "camelcase": "^3.0.0", - "lodash.assign": "^4.0.6" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -12858,6 +12922,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/get-uri/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/get-uri/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -13954,14 +14064,6 @@ "node": ">=10.13.0" } }, - "node_modules/invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -13979,22 +14081,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, - "node_modules/ip-range-check": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/ip-range-check/-/ip-range-check-0.0.1.tgz", - "integrity": "sha512-Ue7mUhrqz3KTwRiyzqeCWpo6yfenviwOEWTxZsLReIxOAbpafcskGrheIgJ25In6DNfEiGG5yTfmgd0hqiH/Kw==", - "dependencies": { - "ipaddr.js": "^1.0.1" - } - }, - "node_modules/ip-range-check/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -14473,11 +14559,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" - }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -14667,6 +14748,24 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/javascript-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", @@ -15874,17 +15973,6 @@ "node": ">=6" } }, - "node_modules/lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", - "dependencies": { - "invert-kv": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -16002,11 +16090,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -16145,9 +16228,9 @@ } }, "node_modules/macos-export-certificate-and-key": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/macos-export-certificate-and-key/-/macos-export-certificate-and-key-1.1.2.tgz", - "integrity": "sha512-kd4ba3kVKZXy46p4tg3X19dmwaXjtz0La5It6Rt6PbtwP+YcQ0F7ab8MjcSHOvz9NSXmAU15qQG53OlBDAPDzQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/macos-export-certificate-and-key/-/macos-export-certificate-and-key-1.2.2.tgz", + "integrity": "sha512-+LwU/wG3wawI3yZ/CMf9C6jSSugJ823EuNJeV8J+FTbmYDJ8G3sF9Fha/0BLEbRZU28+oVvBD3a4mYxLQzDvLA==", "hasInstallScript": true, "optional": true, "os": [ @@ -16544,31 +16627,31 @@ "devOptional": true }, "node_modules/mocha": { - "version": "10.5.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.5.1.tgz", - "integrity": "sha512-eq5tEnaz2kM9ade8cuGJBMh5fBb9Ih/TB+ddlmPR+wLQmwLhUwa0ovqDlg7OTfKquW0BI7NUcNWX7DH8sC+3gw==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", "dev": true, "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", "chokidar": "^3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -16634,29 +16717,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/mocha/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -16721,9 +16781,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -16786,19 +16846,10 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/mongodb": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz", - "integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz", + "integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==", "dependencies": { "@mongodb-js/saslprep": "^1.1.5", "bson": "^6.7.0", @@ -16890,12 +16941,11 @@ } }, "node_modules/mongodb-cloud-info": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mongodb-cloud-info/-/mongodb-cloud-info-2.1.2.tgz", - "integrity": "sha512-t5i0Q/PrUE2ZfFMWEJFyCSDPSmeTKKiwGIkMEpBeNH0Qv0gnVzp6hJ8EWGzcdhLnk7kgHj0x5O7V5oy+emGoAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/mongodb-cloud-info/-/mongodb-cloud-info-2.1.4.tgz", + "integrity": "sha512-g7b6tuEY60IZzdMwzsl20j7ZLCe1DmeAucdmBrZX3yg0LVW24UXIRvqnj/nu7h530PbHkmyJZlElpwt21KN93w==", "dependencies": { "cross-fetch": "^3.1.6", - "gce-ips": "^1.0.2", "ipaddr.js": "^2.1.0" } }, @@ -17037,23 +17087,23 @@ } }, "node_modules/mongodb-redact": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-0.2.3.tgz", - "integrity": "sha512-a6ZPWlC9yf6F/n6ylKyyTO2PXZeD6nPKWwBmAIlOtOH4v82DIfsgO4Bpml10/YSwFxF1+VY8NHohmxofzpB4Yw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-1.1.3.tgz", + "integrity": "sha512-rMw3JlgJ1WhTYUlztojIJ+PGO4sQuwQmsbs1YY60Qf+jPHiDfP10YF//BvpVfy5lKRxAYZmgRTL/fLj1bUbIAg==", "dependencies": { - "lodash": "^4.17.15" + "lodash": "^4.17.21" } }, "node_modules/mongodb-runner": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.2.tgz", - "integrity": "sha512-6XF3iGXswbJy8TC4VgYPVxnrMiUTJ7iaehE+Hiox2sZL2y3b6aNKkrD3Rt2w6nO0JKnwlR/mukyXbMlz2Zmuvw==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.4.tgz", + "integrity": "sha512-pwWsei3btwq9omPLjNaERmSxwt72/OzQx1O468XBuUfhmlTJ+Ta+CIUYOX8vP3K0vSS6E/NUQBG/3ctEoLqOCA==", "dev": true, "dependencies": { - "@mongodb-js/mongodb-downloader": "^0.3.2", - "@mongodb-js/saslprep": "^1.1.7", + "@mongodb-js/mongodb-downloader": "^0.3.3", + "@mongodb-js/saslprep": "^1.1.8", "debug": "^4.3.4", - "mongodb": "^6.3.0", + "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.0", "yargs": "^17.7.2" }, @@ -17331,6 +17381,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -17383,6 +17441,24 @@ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "devOptional": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -17458,14 +17534,15 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -17476,12 +17553,14 @@ "node_modules/normalize-package-data/node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, "bin": { "semver": "bin/semver" } @@ -17495,15 +17574,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -17711,14 +17781,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/numeral": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", @@ -18175,17 +18237,6 @@ "node-addon-api": "^4.3.0" } }, - "node_modules/os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", - "dependencies": { - "lcid": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/os-shim": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", @@ -18269,6 +18320,77 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -18427,6 +18549,40 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -18508,6 +18664,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -18516,6 +18673,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -18524,6 +18682,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, "dependencies": { "pinkie": "^2.0.0" }, @@ -18617,12 +18776,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, "node_modules/postcss/node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -19450,15 +19603,11 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==" - }, "node_modules/require-package-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", @@ -19543,6 +19692,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/resolve-mongodb-srv/-/resolve-mongodb-srv-1.1.5.tgz", "integrity": "sha512-flu1XTSLDJHvTnWu2aJh2w9jgGPcNYJn2obMkuzXiyWSz0MLXu9IRCjvirJ4zRoCPHJJPt3uLQVNJTrzFRWd1w==", + "optional": true, "dependencies": { "whatwg-url": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" }, @@ -19554,6 +19704,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "optional": true, "dependencies": { "punycode": "^2.1.1" }, @@ -19565,6 +19716,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "optional": true, "engines": { "node": ">=12" } @@ -19573,6 +19725,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "optional": true, "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" @@ -20206,9 +20359,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -20231,7 +20384,8 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true }, "node_modules/set-function-length": { "version": "1.1.1", @@ -20803,6 +20957,30 @@ "npm": ">= 3.0.0" } }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/socksv5": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/socksv5/-/socksv5-0.0.6.tgz", @@ -20938,6 +21116,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -20946,12 +21125,14 @@ "node_modules/spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -20960,7 +21141,8 @@ "node_modules/spdx-license-ids": { "version": "3.0.13", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true }, "node_modules/spdx-ranges": { "version": "2.1.1", @@ -21299,6 +21481,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -21392,6 +21589,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -21851,15 +22061,6 @@ "node": ">= 10.13.0" } }, - "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/terser-webpack-plugin/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -21954,15 +22155,12 @@ } }, "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/tmpl": { @@ -22542,9 +22740,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "funding": [ { "type": "opencollective", @@ -22665,6 +22863,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -22739,9 +22938,9 @@ } }, "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", - "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" }, "node_modules/vscode-languageserver-types": { "version": "3.17.3", @@ -22806,6 +23005,14 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -22816,9 +23023,9 @@ } }, "node_modules/webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -23046,11 +23253,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==" - }, "node_modules/which-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", @@ -23096,17 +23298,6 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "optional": true }, - "node_modules/window-size": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha512-UD7d8HFA2+PZsbKyaOCEy8gMh1oDtHgJh1LfgjQ4zVXmYjAT/kvz3PueITKuqDiIXQe7yzpPnxX3lNc+AhQMyw==", - "bin": { - "window-size": "cli.js" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/winston": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz", @@ -23147,9 +23338,9 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -23169,6 +23360,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -24278,11 +24487,11 @@ } }, "@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "requires": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.25.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -24317,19 +24526,17 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", - "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", + "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/traverse": "^7.25.0", "semver": "^6.3.1" }, "dependencies": { @@ -24341,39 +24548,14 @@ } } }, - "@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "requires": { - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "requires": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "requires": { - "@babel/types": "^7.24.7" - } - }, "@babel/helper-member-expression-to-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", - "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", "dev": true, "requires": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" } }, "@babel/helper-module-imports": { @@ -24386,15 +24568,14 @@ } }, "@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "requires": { - "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" } }, "@babel/helper-optimise-call-expression": { @@ -24407,19 +24588,19 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==" + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==" }, "@babel/helper-replace-supers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" } }, "@babel/helper-simple-access": { @@ -24441,18 +24622,10 @@ "@babel/types": "^7.24.7" } }, - "@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "requires": { - "@babel/types": "^7.24.7" - } - }, "@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==" + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==" }, "@babel/helper-validator-identifier": { "version": "7.24.7", @@ -24537,9 +24710,12 @@ } }, "@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==" + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "requires": { + "@babel/types": "^7.25.2" + } }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -24668,21 +24844,21 @@ } }, "@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", - "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", - "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-simple-access": "^7.24.7" } }, @@ -24703,14 +24879,15 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", - "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz", + "integrity": "sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-typescript": "^7.24.7" } }, @@ -24736,38 +24913,35 @@ } }, "@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "requires": { "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" } }, "@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", "requires": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "requires": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" } @@ -25149,6 +25323,71 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -26705,6 +26944,96 @@ } } }, + "@mongodb-js/devtools-proxy-support": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.2.tgz", + "integrity": "sha512-qMSe/5XVEK3xXtMhtv+InIRuanH5nDdDo8yD3gFvsw5pRhI9qM5m06imfgx9X1woAFdntIUNy72lGNi2glbOaA==", + "requires": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==" + }, + "node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "optional": true + }, + "node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "system-ca": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/system-ca/-/system-ca-2.0.1.tgz", + "integrity": "sha512-9ZDV9yl8ph6Op67wDGPr4LykX86usE9x3le+XZSHfVMiiVJ5IRgmCWjLgxyz35ju9H3GDIJJZm4ogAeIfN5cQQ==", + "requires": { + "macos-export-certificate-and-key": "^1.2.0", + "win-export-certificate-and-key": "^2.1.0" + } + }, + "win-export-certificate-and-key": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/win-export-certificate-and-key/-/win-export-certificate-and-key-2.1.0.tgz", + "integrity": "sha512-WeMLa/2uNZcS/HWGKU2G1Gzeh3vHpV/UFvwLhJLKxPHYFAbubxxVcJbqmPXaqySWK1Ymymh16zKK5WYIJ3zgzA==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^3.1.0" + } + } + } + }, "@mongodb-js/mongodb-constants": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.10.2.tgz", @@ -26714,9 +27043,9 @@ } }, "@mongodb-js/mongodb-downloader": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.2.tgz", - "integrity": "sha512-bhMfxzaBy31RveAu7qqON3nVXRHYmxJXyC3lZI+mK+4DhagKZdGHJpMkLmHQRt+wAxMR6ldI9YlcWjHSqceIsQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.3.tgz", + "integrity": "sha512-PIu8qF6J02lQsxfq8vdcfvueb8H5mrB43UaOl1E4AQMrOhm403imAZYKH61qe+7J7WzlVFck0zGI/7brHLHcNQ==", "dev": true, "requires": { "debug": "^4.3.4", @@ -26794,17 +27123,17 @@ "requires": {} }, "@mongodb-js/saslprep": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", - "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", "requires": { "sparse-bitfield": "^3.0.3" } }, "@mongodb-js/sbom-tools": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/sbom-tools/-/sbom-tools-0.7.0.tgz", - "integrity": "sha512-hjc5XrDMVaKdecLzl6IkXT2VO8fudF6aNQLRFlhJ528B5KEKBaahKU4cXnFLV7BRpi1E59FrLg3S7U6bYBfcaw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/sbom-tools/-/sbom-tools-0.7.1.tgz", + "integrity": "sha512-nXUmuUTbdVkbp0LrKOlpToOmAY6S2MSdpuJkhuN7m75gq/UqAmowzQEIWptKbPouaRD2KbncTnXLIDqsYPLm1w==", "dev": true, "requires": { "@octokit/rest": "^20.1.1", @@ -26820,9 +27149,9 @@ } }, "@mongodb-js/signing-utils": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@mongodb-js/signing-utils/-/signing-utils-0.3.4.tgz", - "integrity": "sha512-D9rbB6HMXvBAPu2f3Wy3r2rggrlr7NlNt2hn5rjPM27Q2nA6AKL2mMZNkWCHmehuc3l9jdMWfaQgjvIuMMLMew==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/signing-utils/-/signing-utils-0.3.5.tgz", + "integrity": "sha512-59gZ2dTCtZZDloMOxGU6gEY9T6EEXGAMjvoK7o7ETRvI9mU2B95aq59eh0tSf/HtV+CCbIwsbSy2LqqT5GZqKg==", "dev": true, "requires": { "@types/ssh2": "^1.11.19", @@ -26830,6 +27159,14 @@ "ssh2": "^1.15.0" } }, + "@mongodb-js/socksv5": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@mongodb-js/socksv5/-/socksv5-0.0.10.tgz", + "integrity": "sha512-JDz2fLKsjMiSNUxKrCpGptsgu7DzsXfu4gnUQ3RhUaBS1d4YbLrt6HejpckAiHIAa+niBpZAeiUsoop0IihWsw==", + "requires": { + "ip-address": "^9.0.5" + } + }, "@mongodb-js/ssh-tunnel": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mongodb-js/ssh-tunnel/-/ssh-tunnel-2.2.1.tgz", @@ -26841,19 +27178,19 @@ } }, "@mongosh/arg-parser": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/arg-parser/-/arg-parser-2.2.10.tgz", - "integrity": "sha512-QvXziAmACrFty7id5I0s3ZOetIc1wWhjtpytGMtchEiusTQOO+NJy74UMrxK+NGxKPDRL0MroRIo05/LEfPkJg==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/arg-parser/-/arg-parser-2.2.15.tgz", + "integrity": "sha512-KGYiw5bl8cv6wDSrTCDF1B2PjddPhiD5BkulXvgpkfuD5bB2zTJSgpx+EGGcD60OSDRnMdk0tu9AY8uIExtNvA==", "requires": { - "@mongosh/errors": "2.2.10", - "@mongosh/i18n": "2.2.10", + "@mongosh/errors": "2.2.15", + "@mongosh/i18n": "2.2.15", "mongodb-connection-string-url": "^3.0.1" } }, "@mongosh/async-rewriter2": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/async-rewriter2/-/async-rewriter2-2.2.10.tgz", - "integrity": "sha512-ssY+WxY/oOHPZAIjrYzUYhCr1K2towzshxQpYw3nk5JyjTdvN4gc0xnrWMNLJonEOD/ADTdlneFshQhuh/V1vQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/async-rewriter2/-/async-rewriter2-2.2.15.tgz", + "integrity": "sha512-y7LyjulLYe0QodRa4YIpvpHt23VQWrFGx4C5AD3IVVFhgNd0yxg2bWLIMaFsM7wwgbGJU3BxnVecAnHOgiuRHg==", "requires": { "@babel/core": "^7.22.8", "@babel/plugin-transform-destructuring": "^7.22.5", @@ -26863,109 +27200,114 @@ } }, "@mongosh/autocomplete": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/autocomplete/-/autocomplete-2.2.10.tgz", - "integrity": "sha512-iLrZ1vfA67jKwEWtSKGYAZ7QM5beYFo4AsmiV+KCXwtcZref3dX1OgcjBvRW8dRvSonz8/7RnN9r8QtsTyw2xw==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/autocomplete/-/autocomplete-2.2.15.tgz", + "integrity": "sha512-R1rZVWLNmlOsOVGoHCdAxB0mx7J1A4ElPvzRBWcPW+PSEzlTT/9j0AT87exK/jjUE8ZnkzUw/soh4tqFQIjwAA==", "requires": { "@mongodb-js/mongodb-constants": "^0.10.1", - "@mongosh/shell-api": "2.2.10", + "@mongosh/shell-api": "2.2.15", "semver": "^7.5.4" } }, "@mongosh/browser-runtime-core": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-core/-/browser-runtime-core-2.2.10.tgz", - "integrity": "sha512-tN2TwbA/ANuZllWBQaZPYKjGS4M+F8UbPkHS7b08DeixbJB457Dh2HNDXztmADuLPht82DurRxx6nS0suNFNXQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-core/-/browser-runtime-core-2.2.15.tgz", + "integrity": "sha512-jBy6GizoPEvwDJCl53YDQY5Lv1F4ADL0DEqaFvKk0Ltav8EkvCcsmZTY6Kf9MmgVJGXTawWSxKoj5KLj2QUr4g==", "requires": { - "@mongosh/autocomplete": "2.2.10", - "@mongosh/service-provider-core": "2.2.10", - "@mongosh/shell-api": "2.2.10", - "@mongosh/shell-evaluator": "2.2.10" + "@mongosh/autocomplete": "2.2.15", + "@mongosh/service-provider-core": "2.2.15", + "@mongosh/shell-api": "2.2.15", + "@mongosh/shell-evaluator": "2.2.15" } }, "@mongosh/browser-runtime-electron": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-electron/-/browser-runtime-electron-2.2.10.tgz", - "integrity": "sha512-LmI/VVIrchb51vfTKcFZ/Q4xVzQuHnaU5/LdhRwBaAKaxEp02QwQ0IskOUg6MmU1MwDXFstwhn74nn6ZZeBhWg==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-electron/-/browser-runtime-electron-2.2.15.tgz", + "integrity": "sha512-bMxxjn9F6nxvu/qklOUc9jO9nuK24b5qzMZblHgJilicmFVtUPofZ6B5bEaxYoQHEHyB7RITs/MZmXpV6mDehg==", "requires": { - "@mongosh/browser-runtime-core": "2.2.10", - "@mongosh/service-provider-core": "2.2.10", - "@mongosh/types": "2.2.10" + "@mongosh/browser-runtime-core": "2.2.15", + "@mongosh/service-provider-core": "2.2.15", + "@mongosh/types": "2.2.15" } }, "@mongosh/errors": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/errors/-/errors-2.2.10.tgz", - "integrity": "sha512-jR8dv9MYYRwr+Yri/KI6HAuob0zdVBQOrMvjc+ygBbTIkL3wh1iOrjZKZuYUsjei1FDxLA8NywftAoHDchq2Tg==" + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/errors/-/errors-2.2.15.tgz", + "integrity": "sha512-RHCRv3Fg/xWS5XV4hOyh6KDBrn2kld+J5PVtXfsuke73jfQTLlR2PGMzSEpPWiayRLgLExq56qdXGOtNecmhuA==" }, "@mongosh/history": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/history/-/history-2.2.10.tgz", - "integrity": "sha512-EmMKvsytTXq/1tAwxZUvl+6+gCrarWdEDB9mO9vTCFneOgB0ao2jpo7KA9Jc63r8RYhDH78dtjFsSuFeox3AiA==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/history/-/history-2.2.15.tgz", + "integrity": "sha512-GV1i3RmG38+OUxBnqTeAlcPezkJ4fH3bBs4bwvLEV7iXMcVNzNoJBMyDa7gO6er45w38Kczx9kVDIOYdutt2Yg==", "requires": { "mongodb-connection-string-url": "^3.0.1", - "mongodb-redact": "^0.2.3" + "mongodb-redact": "^1.1.2" } }, "@mongosh/i18n": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/i18n/-/i18n-2.2.10.tgz", - "integrity": "sha512-TxyFOhdXqCN0AXGcWziPzZ+YBf8KthnZIbOVSyA+C1a0jynfMf+XBnLnNpwvKfGZUeQ1VyZkrKLPopG7e2nuIA==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/i18n/-/i18n-2.2.15.tgz", + "integrity": "sha512-7pjQbvJbtaglZKj86/2GRQnXLRekmpTPIVR2M58kAVXaNGqGrfCpe6mkBEkIwdjk6UHQIvkwMSzUIbFGm7nFvA==", "requires": { - "@mongosh/errors": "2.2.10" + "@mongosh/errors": "2.2.15" } }, "@mongosh/service-provider-core": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/service-provider-core/-/service-provider-core-2.2.10.tgz", - "integrity": "sha512-XB+G24WjTIYXnHoToJhDJMwMV7pCcgCwgme0MiE6lzcYs5HwlHhk9tzBwv0+i+3NcCb5SMqU38iUMuGG6jKJdQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/service-provider-core/-/service-provider-core-2.2.15.tgz", + "integrity": "sha512-Pk+Sxxf0rE7KacEMZvhGjr15cWkV+lcbI8cv5Hf7Taxj8kLXfbKM45WBIgGtMDTh/fbmbT15qI7StG5sCO8CCg==", "requires": { "@aws-sdk/credential-providers": "^3.525.0", - "@mongosh/errors": "2.2.10", + "@mongosh/errors": "2.2.15", "bson": "^6.7.0", - "mongodb": "^6.7.0", + "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", "mongodb-client-encryption": "^6.0.0" } }, "@mongosh/service-provider-server": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/service-provider-server/-/service-provider-server-2.2.10.tgz", - "integrity": "sha512-7WxxKZgXNdW7f6vUCmqqWPexPOFqy/n155rFeyWhRVBI5mdbb/Sb5vsdqo1+6AyDpCXsLdl8CldsqmOfGKG36A==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/service-provider-server/-/service-provider-server-2.2.15.tgz", + "integrity": "sha512-TtQVsMlQhZqRQ6Aj9tUcSvNLL6tthkzx3m/FhBxyT/7rxxuK56w0IUVg31Cqq/gz9xUfkP3JiKounIgYqRCbXQ==", "requires": { - "@mongodb-js/devtools-connect": "^3.0.1", + "@mongodb-js/devtools-connect": "^3.0.5", "@mongodb-js/oidc-plugin": "^1.0.2", - "@mongosh/errors": "2.2.10", - "@mongosh/service-provider-core": "2.2.10", - "@mongosh/types": "2.2.10", + "@mongosh/errors": "2.2.15", + "@mongosh/service-provider-core": "2.2.15", + "@mongosh/types": "2.2.15", "aws4": "^1.12.0", "kerberos": "^2.1.0", - "mongodb": "^6.7.0", + "mongodb": "^6.8.0", "mongodb-client-encryption": "^6.0.0", "mongodb-connection-string-url": "^3.0.1", "socks": "^2.8.3" }, "dependencies": { "@mongodb-js/devtools-connect": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.0.1.tgz", - "integrity": "sha512-xbk/eGHPQTQz4VSpGb5oRqSSbzipcFDODrAc4YtYFrb0980buOAopO71NozCbQoVnoiO1pYVIqcnrZMHkdaJzg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.2.tgz", + "integrity": "sha512-0d/9hiNnVxFjgu0HtbUSMOem/hMtpj7aKj/QN3UsABGJ8jBxMXzE90jNP6JOJ+Nt5dmlCX2iTMvtJiBIWOtCZA==", "requires": { - "@mongodb-js/oidc-http-server-pages": "1.1.1", + "@mongodb-js/devtools-proxy-support": "^0.3.2", + "@mongodb-js/oidc-http-server-pages": "1.1.2", "kerberos": "^2.1.0", "lodash.merge": "^4.6.2", - "mongodb-client-encryption": "^6.0.0", + "mongodb-client-encryption": "^6.0.0 || ^6.1.0-alpha.0", "mongodb-connection-string-url": "^3.0.0", "os-dns-native": "^1.2.0", "resolve-mongodb-srv": "^1.1.1", - "socks": "^2.7.3", - "system-ca": "^1.0.2" + "socks": "^2.7.3" } }, + "@mongodb-js/oidc-http-server-pages": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.1.2.tgz", + "integrity": "sha512-YcRlJ/HkKMahWvaPFTN/al5MGGX6CKQTsBHFTpNduxmeHd40jO8Cj5W7dfrqJAoit5E/G+GOwMaEfWdNrdlihg==" + }, "@mongodb-js/oidc-plugin": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.0.2.tgz", - "integrity": "sha512-hwTbkmJ31RPB5ksA6pLepnaQOBz6iurE+uH89B1IIJdxVuiO0Qz+OqpTN8vk8LZzcVDb/WbNoxqxogCWwMqFKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", + "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", "requires": { "express": "^4.18.2", "open": "^9.1.0", @@ -26975,56 +27317,61 @@ } }, "@mongosh/shell-api": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/shell-api/-/shell-api-2.2.10.tgz", - "integrity": "sha512-Cgcb0U5wqzmTmwAmMkSqhZ3fR4PjqNJ2px61i/9JzWAgIEDSbv9Xni3mcfDRQd/qjLHCEUlAPFgEBu7Cpk0qBQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/shell-api/-/shell-api-2.2.15.tgz", + "integrity": "sha512-HkJhDKWHRRqa7fznsRVp/ivolM7RKeCyTuJXMVFym3qt4wlC63Tc3IQjm8HYORlFGRz04AOOwCgzkIp8ddPXkg==", "requires": { - "@mongosh/arg-parser": "2.2.10", - "@mongosh/errors": "2.2.10", - "@mongosh/history": "2.2.10", - "@mongosh/i18n": "2.2.10", - "@mongosh/service-provider-core": "2.2.10", - "mongodb-redact": "^0.2.3" + "@mongosh/arg-parser": "2.2.15", + "@mongosh/errors": "2.2.15", + "@mongosh/history": "2.2.15", + "@mongosh/i18n": "2.2.15", + "@mongosh/service-provider-core": "2.2.15", + "mongodb-redact": "^1.1.2" } }, "@mongosh/shell-evaluator": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/shell-evaluator/-/shell-evaluator-2.2.10.tgz", - "integrity": "sha512-9v/p5WDu+Ur7+jhXajkBOvpINcRORA1UpdMOV4sBGQ623VbmoDd7xFeFPoi0uWzV50qW5yNRlh3+dvsG/jdKmQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/shell-evaluator/-/shell-evaluator-2.2.15.tgz", + "integrity": "sha512-Km/rThnbklPiYfNd/K1qFUNXICMRaYVq1pOWWSYbrT7a97KcFHIoD2OgUUudksuva4zc24CfeP5GSWRtYpbq+w==", "requires": { - "@mongosh/async-rewriter2": "2.2.10", - "@mongosh/history": "2.2.10", - "@mongosh/shell-api": "2.2.10" + "@mongosh/async-rewriter2": "2.2.15", + "@mongosh/history": "2.2.15", + "@mongosh/shell-api": "2.2.15" } }, "@mongosh/types": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@mongosh/types/-/types-2.2.10.tgz", - "integrity": "sha512-BfCcmbC3givNCgYZxf6aUJy+/nHg2By6Hs9gZ/WMGgiedMuL5fRE18dGlwy3Aog7Jc6tVkBCMtOpYgjVnUPoxQ==", + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@mongosh/types/-/types-2.2.15.tgz", + "integrity": "sha512-HkhZkjrkK9w+QHd2kPl7mspZUOpCUmgEvvHLMHmhpaYksLcxm2H4/H+s5F1Kj3EpuC9yyOHuvfC3ZMhDOgF0tg==", "requires": { - "@mongodb-js/devtools-connect": "^3.0.1" + "@mongodb-js/devtools-connect": "^3.0.5" }, "dependencies": { "@mongodb-js/devtools-connect": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.0.1.tgz", - "integrity": "sha512-xbk/eGHPQTQz4VSpGb5oRqSSbzipcFDODrAc4YtYFrb0980buOAopO71NozCbQoVnoiO1pYVIqcnrZMHkdaJzg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.2.tgz", + "integrity": "sha512-0d/9hiNnVxFjgu0HtbUSMOem/hMtpj7aKj/QN3UsABGJ8jBxMXzE90jNP6JOJ+Nt5dmlCX2iTMvtJiBIWOtCZA==", "requires": { - "@mongodb-js/oidc-http-server-pages": "1.1.1", + "@mongodb-js/devtools-proxy-support": "^0.3.2", + "@mongodb-js/oidc-http-server-pages": "1.1.2", "kerberos": "^2.1.0", "lodash.merge": "^4.6.2", - "mongodb-client-encryption": "^6.0.0", + "mongodb-client-encryption": "^6.0.0 || ^6.1.0-alpha.0", "mongodb-connection-string-url": "^3.0.0", "os-dns-native": "^1.2.0", "resolve-mongodb-srv": "^1.1.1", - "socks": "^2.7.3", - "system-ca": "^1.0.2" + "socks": "^2.7.3" } }, + "@mongodb-js/oidc-http-server-pages": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.1.2.tgz", + "integrity": "sha512-YcRlJ/HkKMahWvaPFTN/al5MGGX6CKQTsBHFTpNduxmeHd40jO8Cj5W7dfrqJAoit5E/G+GOwMaEfWdNrdlihg==" + }, "@mongodb-js/oidc-plugin": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.0.2.tgz", - "integrity": "sha512-hwTbkmJ31RPB5ksA6pLepnaQOBz6iurE+uH89B1IIJdxVuiO0Qz+OqpTN8vk8LZzcVDb/WbNoxqxogCWwMqFKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", + "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", "peer": true, "requires": { "express": "^4.18.2", @@ -27194,6 +27541,13 @@ "yargs": "^16.2.0" } }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", @@ -27917,6 +28271,11 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -28005,9 +28364,9 @@ } }, "@types/chai": { - "version": "4.3.16", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.16.tgz", - "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", + "version": "4.3.17", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.17.tgz", + "integrity": "sha512-zmZ21EWzR71B4Sscphjief5djsLre50M6lI622OSySTmn9DB3j+C3kWroHfBQWXbOBwbgg/M8CG/hUxDLIloow==", "dev": true }, "@types/debug": { @@ -28140,9 +28499,9 @@ } }, "@types/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-C/FMQ8HJAZhTsDpl4wDKZdMeeW5USjgzOczUwTGbRc1ZopPgOhIEnxY2ZgUrsuyy4DwK1JVOJZKFakv3TbCKiA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz", + "integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==", "dev": true, "requires": { "@types/braces": "*" @@ -28326,9 +28685,9 @@ "dev": true }, "@types/vscode": { - "version": "1.90.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.90.0.tgz", - "integrity": "sha512-oT+ZJL7qHS9Z8bs0+WKf/kQ27qWYR3trsXpq46YDjFqBsMLG4ygGGjPaJ2tyrH0wJzjOEmDyg9PDJBBhWg9pkQ==", + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz", + "integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==", "dev": true }, "@types/webidl-conversions": { @@ -28474,13 +28833,13 @@ "dev": true }, "@vscode/test-electron": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.0.tgz", - "integrity": "sha512-yojuDFEjohx6Jb+x949JRNtSn6Wk2FAh4MldLE3ck9cfvCqzwxF32QsNy1T9Oe4oT+ZfFcg0uPUCajJzOmPlTA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz", + "integrity": "sha512-Gc6EdaLANdktQ1t+zozoBVRynfIsMKMc94Svu1QreOBC8y76x4tvaK32TljrLi1LI2+PK58sDVbL7ALdqf3VRQ==", "dev": true, "requires": { "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", + "https-proxy-agent": "^7.0.5", "jszip": "^3.10.1", "ora": "^7.0.1", "semver": "^7.6.2" @@ -28533,9 +28892,9 @@ } }, "https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "requires": { "agent-base": "^7.0.2", @@ -28614,9 +28973,9 @@ } }, "@vscode/vsce": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.29.0.tgz", - "integrity": "sha512-63+aEO8SpjE6qKiIh2Cqy/P9zC7+USElGwpEdkyPp89xIBDBr5IqeNS3zkD3mp3wZqbvHIpJsCCNu74WQirYCg==", + "version": "2.31.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.31.1.tgz", + "integrity": "sha512-LwEQFKXV21C4/brvGPH/9+7ZOUM5cbK7oJ4fVmy0YG75NIy1HV8eMSoBZrl+u23NxpAhor62Cu1aI+JFtCtjSg==", "dev": true, "requires": { "@azure/identity": "^4.1.0", @@ -28627,7 +28986,7 @@ "cockatiel": "^3.1.2", "commander": "^6.2.1", "form-data": "^4.0.0", - "glob": "^7.0.6", + "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "keytar": "^7.7.0", @@ -28638,7 +28997,7 @@ "parse-semver": "^1.1.1", "read": "^1.0.7", "semver": "^7.5.2", - "tmp": "^0.2.1", + "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", @@ -28655,6 +29014,15 @@ "color-convert": "^1.9.0" } }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -28693,12 +29061,43 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "dependencies": { + "minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -29160,9 +29559,9 @@ "requires": {} }, "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "ansi-escapes": { @@ -29389,17 +29788,20 @@ "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", "dev": true }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "requires": { + "tslib": "^2.0.1" + } + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -29412,20 +29814,6 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, - "autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", - "dev": true, - "requires": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -29606,6 +29994,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -29875,14 +30268,14 @@ } }, "browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "requires": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" } }, "bs-logger": { @@ -29904,9 +30297,9 @@ } }, "bson": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", - "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==" + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==" }, "bson-transpilers": { "version": "2.2.0", @@ -30083,9 +30476,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001636", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", - "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==" + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==" }, "capture-exit": { "version": "2.0.0", @@ -30097,9 +30490,9 @@ } }, "chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "requires": { "assertion-error": "^1.1.0", @@ -30108,7 +30501,15 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" + }, + "dependencies": { + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + } } }, "chai-as-promised": { @@ -30431,11 +30832,6 @@ "integrity": "sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==", "dev": true }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" - }, "collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -30779,6 +31175,11 @@ "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==" }, + "data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==" + }, "data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -30797,9 +31198,9 @@ "dev": true }, "debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "requires": { "ms": "2.1.2" }, @@ -31122,6 +31523,16 @@ "isobject": "^3.0.1" } }, + "degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -31253,9 +31664,9 @@ "optional": true }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true }, "diff-sequences": { @@ -31558,9 +31969,9 @@ } }, "electron-to-chromium": { - "version": "1.4.811", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.811.tgz", - "integrity": "sha512-CDyzcJ5XW78SHzsIOdn27z8J4ist8eaFLhdto2hSMSJQgsiwvbv2fbizcKUICryw1Wii1TI/FEkvzvJsR3awrA==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", + "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==" }, "elliptic": { "version": "6.5.4", @@ -31833,7 +32244,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, "requires": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -31845,7 +32255,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "optional": true } } @@ -32304,9 +32713,9 @@ "requires": {} }, "eslint-plugin-mocha": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.4.3.tgz", - "integrity": "sha512-emc4TVjq5Ht0/upR+psftuz6IBG5q279p+1dSRDeHf+NS9aaerBi3lXKo1SEzwC29hFIW21gO89CEWSvRsi8IQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", "dev": true, "requires": { "eslint-utils": "^3.0.0", @@ -32460,8 +32869,7 @@ "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" }, "estree-walker": { "version": "2.0.2", @@ -32472,8 +32880,7 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "etag": { "version": "1.8.1", @@ -32967,6 +33374,15 @@ "pend": "~1.2.0" } }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -33122,6 +33538,24 @@ "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true }, + "foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "fork-ts-checker-webpack-plugin": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", @@ -33209,17 +33643,19 @@ "mime-types": "^2.1.12" } }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, - "fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true - }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -33322,193 +33758,6 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" }, - "gce-ips": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/gce-ips/-/gce-ips-1.0.2.tgz", - "integrity": "sha512-cg2Mxsqdk3Ja2VkYgvvPgXN0+PoS0Sx1ts3J7oLV/umOybgNSfsEsRiqNIItdv0jYPXQx/5xwziGYNZ8s6SiRA==", - "requires": { - "async": "^1.5.2", - "ip-range-check": "0.0.1", - "lodash.assign": "^4.0.8", - "yargs": "^4.7.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==" - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "requires": { - "is-utf8": "^0.2.0" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" - }, - "yargs": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", - "integrity": "sha512-LqodLrnIDM3IFT+Hf/5sxBnEGECrfdC1uIbgZeJmESCSo4HoCAaKEus8MylXHAkdacGc0ye+Qa+dpkuom8uVYA==", - "requires": { - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "lodash.assign": "^4.0.3", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.1", - "which-module": "^1.0.0", - "window-size": "^0.2.0", - "y18n": "^3.2.1", - "yargs-parser": "^2.4.1" - } - }, - "yargs-parser": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", - "integrity": "sha512-9pIKIJhnI5tonzG6OnCFlz/yln8xHYcGl+pn3xR0Vzff0vzN1PbNRaelgfgRUwZ3s4i3jvxT9WhmUGL4whnasA==", - "requires": { - "camelcase": "^3.0.0", - "lodash.assign": "^4.0.6" - } - } - } - }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -33562,6 +33811,43 @@ "get-intrinsic": "^1.1.1" } }, + "get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "requires": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + } + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -34399,11 +34685,6 @@ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==" - }, "ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -34420,21 +34701,6 @@ } } }, - "ip-range-check": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/ip-range-check/-/ip-range-check-0.0.1.tgz", - "integrity": "sha512-Ue7mUhrqz3KTwRiyzqeCWpo6yfenviwOEWTxZsLReIxOAbpafcskGrheIgJ25In6DNfEiGG5yTfmgd0hqiH/Kw==", - "requires": { - "ipaddr.js": "^1.0.1" - }, - "dependencies": { - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - } - } - }, "ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -34753,11 +35019,6 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" - }, "is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -34905,6 +35166,16 @@ "istanbul-lib-report": "^3.0.0" } }, + "jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "javascript-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", @@ -35904,14 +36175,6 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", - "requires": { - "invert-kv": "^1.0.0" - } - }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -36007,11 +36270,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" - }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -36135,9 +36393,9 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==" }, "macos-export-certificate-and-key": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/macos-export-certificate-and-key/-/macos-export-certificate-and-key-1.1.2.tgz", - "integrity": "sha512-kd4ba3kVKZXy46p4tg3X19dmwaXjtz0La5It6Rt6PbtwP+YcQ0F7ab8MjcSHOvz9NSXmAU15qQG53OlBDAPDzQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/macos-export-certificate-and-key/-/macos-export-certificate-and-key-1.2.2.tgz", + "integrity": "sha512-+LwU/wG3wawI3yZ/CMf9C6jSSugJ823EuNJeV8J+FTbmYDJ8G3sF9Fha/0BLEbRZU28+oVvBD3a4mYxLQzDvLA==", "optional": true, "requires": { "bindings": "^1.5.0", @@ -36451,31 +36709,31 @@ "devOptional": true }, "mocha": { - "version": "10.5.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.5.1.tgz", - "integrity": "sha512-eq5tEnaz2kM9ade8cuGJBMh5fBb9Ih/TB+ddlmPR+wLQmwLhUwa0ovqDlg7OTfKquW0BI7NUcNWX7DH8sC+3gw==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", "dev": true, "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", "chokidar": "^3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "dependencies": { "argparse": { @@ -36484,23 +36742,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -36543,9 +36784,9 @@ } }, "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -36588,12 +36829,6 @@ "requires": { "has-flag": "^4.0.0" } - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true } } }, @@ -36632,9 +36867,9 @@ } }, "mongodb": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz", - "integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz", + "integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==", "requires": { "@mongodb-js/saslprep": "^1.1.5", "bson": "^6.7.0", @@ -36683,12 +36918,11 @@ } }, "mongodb-cloud-info": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mongodb-cloud-info/-/mongodb-cloud-info-2.1.2.tgz", - "integrity": "sha512-t5i0Q/PrUE2ZfFMWEJFyCSDPSmeTKKiwGIkMEpBeNH0Qv0gnVzp6hJ8EWGzcdhLnk7kgHj0x5O7V5oy+emGoAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/mongodb-cloud-info/-/mongodb-cloud-info-2.1.4.tgz", + "integrity": "sha512-g7b6tuEY60IZzdMwzsl20j7ZLCe1DmeAucdmBrZX3yg0LVW24UXIRvqnj/nu7h530PbHkmyJZlElpwt21KN93w==", "requires": { "cross-fetch": "^3.1.6", - "gce-ips": "^1.0.2", "ipaddr.js": "^2.1.0" } }, @@ -36810,23 +37044,23 @@ } }, "mongodb-redact": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-0.2.3.tgz", - "integrity": "sha512-a6ZPWlC9yf6F/n6ylKyyTO2PXZeD6nPKWwBmAIlOtOH4v82DIfsgO4Bpml10/YSwFxF1+VY8NHohmxofzpB4Yw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-1.1.3.tgz", + "integrity": "sha512-rMw3JlgJ1WhTYUlztojIJ+PGO4sQuwQmsbs1YY60Qf+jPHiDfP10YF//BvpVfy5lKRxAYZmgRTL/fLj1bUbIAg==", "requires": { - "lodash": "^4.17.15" + "lodash": "^4.17.21" } }, "mongodb-runner": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.2.tgz", - "integrity": "sha512-6XF3iGXswbJy8TC4VgYPVxnrMiUTJ7iaehE+Hiox2sZL2y3b6aNKkrD3Rt2w6nO0JKnwlR/mukyXbMlz2Zmuvw==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.4.tgz", + "integrity": "sha512-pwWsei3btwq9omPLjNaERmSxwt72/OzQx1O468XBuUfhmlTJ+Ta+CIUYOX8vP3K0vSS6E/NUQBG/3ctEoLqOCA==", "dev": true, "requires": { - "@mongodb-js/mongodb-downloader": "^0.3.2", - "@mongodb-js/saslprep": "^1.1.7", + "@mongodb-js/mongodb-downloader": "^0.3.3", + "@mongodb-js/saslprep": "^1.1.8", "debug": "^4.3.4", - "mongodb": "^6.3.0", + "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.0", "yargs": "^17.7.2" }, @@ -37058,6 +37292,11 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -37109,6 +37348,11 @@ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "devOptional": true }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -37172,14 +37416,15 @@ } }, "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -37190,12 +37435,14 @@ "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true } } }, @@ -37205,12 +37452,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true - }, "normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -37366,11 +37607,6 @@ "boolbase": "^1.0.0" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" - }, "numeral": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", @@ -37709,14 +37945,6 @@ "node-addon-api": "^4.3.0" } }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", - "requires": { - "lcid": "^1.0.0" - } - }, "os-shim": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", @@ -37770,6 +37998,64 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "requires": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + } + } + }, + "pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "requires": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + } + }, + "package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -37894,6 +38180,30 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "dependencies": { + "lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + } + } + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -37953,17 +38263,20 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true }, "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==" + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, "requires": { "pinkie": "^2.0.0" } @@ -38030,12 +38343,6 @@ } } }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, "pre-commit": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", @@ -38675,12 +38982,8 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==" + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true }, "require-package-name": { "version": "2.0.1", @@ -38750,6 +39053,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/resolve-mongodb-srv/-/resolve-mongodb-srv-1.1.5.tgz", "integrity": "sha512-flu1XTSLDJHvTnWu2aJh2w9jgGPcNYJn2obMkuzXiyWSz0MLXu9IRCjvirJ4zRoCPHJJPt3uLQVNJTrzFRWd1w==", + "optional": true, "requires": { "whatwg-url": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" }, @@ -38758,6 +39062,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "optional": true, "requires": { "punycode": "^2.1.1" } @@ -38765,12 +39070,14 @@ "webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "optional": true }, "whatwg-url": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "optional": true, "requires": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" @@ -39257,9 +39564,9 @@ } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -39279,7 +39586,8 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true }, "set-function-length": { "version": "1.1.1", @@ -39731,6 +40039,26 @@ "smart-buffer": "^4.2.0" } }, + "socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "requires": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + } + } + }, "socksv5": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/socksv5/-/socksv5-0.0.6.tgz", @@ -39843,6 +40171,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -39851,12 +40180,14 @@ "spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true }, "spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -39865,7 +40196,8 @@ "spdx-license-ids": { "version": "3.0.13", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true }, "spdx-ranges": { "version": "2.1.1", @@ -40143,6 +40475,17 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -40212,6 +40555,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -40571,15 +40923,6 @@ "supports-color": "^8.0.0" } }, - "serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -40654,13 +40997,10 @@ "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==" }, "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true }, "tmpl": { "version": "1.0.5", @@ -41100,9 +41440,9 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==" }, "update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "requires": { "escalade": "^3.1.2", "picocolors": "^1.0.1" @@ -41189,6 +41529,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -41250,9 +41591,9 @@ } }, "vscode-languageserver-textdocument": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", - "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" }, "vscode-languageserver-types": { "version": "3.17.3", @@ -41310,6 +41651,11 @@ "defaults": "^1.0.3" } }, + "web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" + }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -41317,9 +41663,9 @@ "dev": true }, "webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", @@ -41477,11 +41823,6 @@ "is-weakset": "^2.0.1" } }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==" - }, "which-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", @@ -41519,11 +41860,6 @@ } } }, - "window-size": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha512-UD7d8HFA2+PZsbKyaOCEy8gMh1oDtHgJh1LfgjQ4zVXmYjAT/kvz3PueITKuqDiIXQe7yzpPnxX3lNc+AhQMyw==" - }, "winston": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz", @@ -41557,9 +41893,9 @@ "dev": true }, "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "wrap-ansi": { @@ -41573,6 +41909,17 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index f60d77464..9163a2df3 100644 --- a/package.json +++ b/package.json @@ -68,9 +68,9 @@ "create-vulnerability-tickets": "mongodb-sbom-tools generate-vulnerability-report --snyk-reports=.sbom/snyk-test-result.json --dependencies=.sbom/dependencies.json --create-jira-issues" }, "engines": { - "vscode": "^1.90.2", - "node": ">=16.16.0", - "npm": ">=8.19.4" + "vscode": "^1.92.1", + "node": ">=20.9.0", + "npm": ">=10.1.0" }, "activationEvents": [ "onView:mongoDB", @@ -1115,20 +1115,20 @@ "@mongodb-js/connection-form": "^1.22.2", "@mongodb-js/connection-info": "^0.1.2", "@mongodb-js/mongodb-constants": "^0.10.2", - "@mongosh/browser-runtime-electron": "^2.2.10", - "@mongosh/i18n": "^2.2.10", - "@mongosh/service-provider-server": "^2.2.10", - "@mongosh/shell-api": "^2.2.10", + "@mongosh/browser-runtime-electron": "^2.2.15", + "@mongosh/i18n": "^2.2.15", + "@mongosh/service-provider-server": "^2.2.15", + "@mongosh/shell-api": "^2.2.15", "@segment/analytics-node": "^1.3.0", - "bson": "^6.7.0", + "bson": "^6.8.0", "bson-transpilers": "^2.2.0", - "debug": "^4.3.5", + "debug": "^4.3.6", "dotenv": "^16.4.5", "lodash": "^4.17.21", "micromatch": "^4.0.7", - "mongodb": "^6.7.0", + "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", - "mongodb-cloud-info": "^2.1.2", + "mongodb-cloud-info": "^2.1.4", "mongodb-connection-string-url": "^3.0.1", "mongodb-data-service": "^22.21.1", "mongodb-log-writer": "^1.4.2", @@ -1137,29 +1137,28 @@ "numeral": "^2.0.6", "react": "^17.0.2", "react-dom": "^17.0.2", - "resolve-mongodb-srv": "^1.1.5", "ts-log": "^2.2.5", "uuid": "^8.3.2", "vscode-languageclient": "^8.1.0", "vscode-languageserver": "^8.1.0", - "vscode-languageserver-textdocument": "^1.0.11" + "vscode-languageserver-textdocument": "^1.0.12" }, "devDependencies": { "@babel/preset-typescript": "^7.24.7", + "@babel/types": "^7.25.2", "@mongodb-js/oidc-mock-provider": "^0.9.1", "@mongodb-js/oidc-plugin": "^0.4.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", - "@mongodb-js/sbom-tools": "^0.7.0", - "@mongodb-js/signing-utils": "^0.3.4", - "@mongosh/service-provider-core": "^2.2.10", + "@mongodb-js/sbom-tools": "^0.7.1", + "@mongodb-js/signing-utils": "^0.3.5", + "@mongosh/service-provider-core": "^2.2.15", "@testing-library/react": "^12.1.5", - "@types/babel__core": "^7.20.5", "@types/babel__traverse": "^7.20.6", - "@types/chai": "^4.3.16", + "@types/chai": "^4.3.17", "@types/debug": "^4.1.12", "@types/glob": "^7.2.0", "@types/jest": "^26.0.24", - "@types/micromatch": "^4.0.7", + "@types/micromatch": "^4.0.9", "@types/mkdirp": "^2.0.0", "@types/mocha": "^8.2.3", "@types/node": "^14.18.63", @@ -1168,15 +1167,14 @@ "@types/sinon": "^9.0.11", "@types/sinon-chai": "^3.2.12", "@types/uuid": "^8.3.4", - "@types/vscode": "^1.90.0", + "@types/vscode": "^1.92.0", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", - "@vscode/test-electron": "^2.4.0", - "@vscode/vsce": "^2.29.0", + "@vscode/test-electron": "^2.4.1", + "@vscode/vsce": "^2.31.1", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", - "autoprefixer": "^10.4.19", "buffer": "^6.0.3", - "chai": "^4.4.1", + "chai": "^4.5.0", "chai-as-promised": "^7.1.2", "cross-env": "^7.0.3", "crypto-browserify": "^3.12.0", @@ -1185,18 +1183,18 @@ "enzyme": "^3.11.0", "eslint": "^8.57.0", "eslint-config-mongodb-js": "^5.0.3", - "eslint-plugin-mocha": "^10.4.3", + "eslint-plugin-mocha": "^10.5.0", "fork-ts-checker-webpack-plugin": "^9.0.2", "glob": "^7.2.3", "jest": "^26.6.3", "jest-junit": "^12.3.0", "jest-transform-stub": "^2.0.0", "mkdirp": "^1.0.4", - "mocha": "^10.5.1", + "mocha": "^10.7.3", "mocha-junit-reporter": "^2.2.1", "mocha-multi": "^1.1.7", "mongodb-client-encryption": "^6.0.1", - "mongodb-runner": "^5.6.2", + "mongodb-runner": "^5.6.4", "node-fetch": "^2.7.0", "node-loader": "^0.6.0", "npm-run-all": "^4.1.5", @@ -1208,11 +1206,12 @@ "sinon": "^9.2.4", "sinon-chai": "^3.7.0", "stream-browserify": "^3.0.0", + "terser-webpack-plugin": "^5.3.10", "ts-jest": "^26.5.6", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "typescript": "^4.9.5", - "webpack": "^5.92.1", + "webpack": "^5.93.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.10.0", diff --git a/src/language/visitor.ts b/src/language/visitor.ts index 9cda0aa46..b6adf7c7d 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -1,6 +1,7 @@ -import type * as babel from '@babel/core'; import * as parser from '@babel/parser'; +import type { NodePath } from '@babel/traverse'; import traverse from '@babel/traverse'; +import type * as t from '@babel/types'; const PLACEHOLDER = 'TRIGGER_CHARACTER'; @@ -14,10 +15,7 @@ export interface VisitorTextAndSelection { selection: VisitorSelection; } -type ObjectKey = - | babel.types.ObjectProperty - | babel.types.SpreadElement - | babel.types.ObjectMethod; +type ObjectKey = t.ObjectProperty | t.SpreadElement | t.ObjectMethod; export interface CompletionState { databaseName: string | null; @@ -62,7 +60,7 @@ export class Visitor { }; } - _visitCallExpression(path: babel.NodePath): void { + _visitCallExpression(path: NodePath): void { if (path.node.type !== 'CallExpression') { return; } @@ -74,7 +72,7 @@ export class Visitor { this._checkHasDatabaseName(path.node); } - _visitMemberExpression(path: babel.NodePath): void { + _visitMemberExpression(path: NodePath): void { if (path.node.type !== 'MemberExpression') { return; } @@ -89,7 +87,7 @@ export class Visitor { this._checkHasStreamProcessorName(path.node); } - _visitExpressionStatement(path: babel.NodePath): void { + _visitExpressionStatement(path: NodePath): void { if (path.node.type === 'ExpressionStatement') { this._checkIsGlobalSymbol(path.node); this._checkIsDbSymbol(path.node); @@ -97,7 +95,7 @@ export class Visitor { } } - _visitObjectExpression(path: babel.NodePath): void { + _visitObjectExpression(path: NodePath): void { if (path.node.type === 'ObjectExpression') { this._checkIsObjectKey(path.node); this._checkIsIdentifierObjectValue(path.node); @@ -105,7 +103,7 @@ export class Visitor { } } - _visitArrayExpression(path: babel.NodePath): void { + _visitArrayExpression(path: NodePath): void { if (path.node.type === 'ArrayExpression') { this._checkIsBSONSelection(path.node); this._checkIsStage(path.node); @@ -113,13 +111,13 @@ export class Visitor { } } - _visitVariableDeclarator(path: babel.NodePath): void { + _visitVariableDeclarator(path: NodePath): void { if (path.node.type === 'VariableDeclarator') { this._checkIsBSONSelection(path.node); } } - _visitObjectProperty(path: babel.NodePath): void { + _visitObjectProperty(path: NodePath): void { if (path.node.type === 'ObjectProperty') { this._checkIsBSONSelection(path.node); } @@ -192,7 +190,7 @@ export class Visitor { } traverse(ast, { - enter: (path: babel.NodePath) => { + enter: (path: NodePath) => { this._visitCallExpression(path); this._visitMemberExpression(path); this._visitExpressionStatement(path); @@ -243,7 +241,7 @@ export class Visitor { }; } - _checkIsUseCallAsSimpleString(node: babel.types.CallExpression): void { + _checkIsUseCallAsSimpleString(node: t.CallExpression): void { if ( node.callee.type === 'Identifier' && node.callee.name === 'use' && @@ -256,7 +254,7 @@ export class Visitor { } } - _checkIsUseCallAsTemplate(node: babel.types.CallExpression): void { + _checkIsUseCallAsTemplate(node: t.CallExpression): void { if ( node.callee.type === 'Identifier' && node.callee.name === 'use' && @@ -272,12 +270,12 @@ export class Visitor { } } - _checkIsUseCall(node: babel.types.CallExpression): void { + _checkIsUseCall(node: t.CallExpression): void { this._checkIsUseCallAsSimpleString(node); this._checkIsUseCallAsTemplate(node); } - _checkIsGlobalSymbol(node: babel.types.ExpressionStatement): void { + _checkIsGlobalSymbol(node: t.ExpressionStatement): void { if ( node.expression.type === 'Identifier' && node.expression.name.includes('TRIGGER_CHARACTER') && @@ -287,7 +285,7 @@ export class Visitor { } } - _checkIsDbSymbol(node: babel.types.ExpressionStatement): void { + _checkIsDbSymbol(node: t.ExpressionStatement): void { if ( node.expression.type === 'MemberExpression' && node.expression.object.type === 'Identifier' && @@ -298,7 +296,7 @@ export class Visitor { } } - _checkIsSpSymbol(node: babel.types.ExpressionStatement): void { + _checkIsSpSymbol(node: t.ExpressionStatement): void { if ( node.expression.type === 'MemberExpression' && node.expression.object.type === 'Identifier' && @@ -309,7 +307,7 @@ export class Visitor { } } - _checkIsObjectKey(node: babel.types.ObjectExpression): void { + _checkIsObjectKey(node: t.ObjectExpression): void { node.properties.find((item: ObjectKey) => { if ( item.type === 'ObjectProperty' && @@ -322,7 +320,7 @@ export class Visitor { }); } - _checkIsIdentifierObjectValue(node: babel.types.ObjectExpression): void { + _checkIsIdentifierObjectValue(node: t.ObjectExpression): void { node.properties.find((item: ObjectKey) => { if ( item.type === 'ObjectProperty' && @@ -335,7 +333,7 @@ export class Visitor { }); } - _checkIsTextObjectValue(node: babel.types.ObjectExpression): void { + _checkIsTextObjectValue(node: t.ObjectExpression): void { node.properties.find((item: ObjectKey) => { if ( ((item.type === 'ObjectProperty' && @@ -352,7 +350,7 @@ export class Visitor { }); } - _checkIsStage(node: babel.types.ArrayExpression): void { + _checkIsStage(node: t.ArrayExpression): void { if (node.elements) { node.elements.forEach((item) => { if (item?.type === 'ObjectExpression') { @@ -371,7 +369,7 @@ export class Visitor { } } - _checkIsStageOperator(path: babel.NodePath): void { + _checkIsStageOperator(path: NodePath): void { if (path.node.type === 'ArrayExpression' && path.node.elements) { path.node.elements.forEach((item) => { if (item?.type === 'ObjectExpression') { @@ -383,7 +381,7 @@ export class Visitor { ) { const name = item.key.name; path.scope.traverse(item, { - enter: (path: babel.NodePath) => { + enter: (path: NodePath) => { if ( path.node.type === 'ObjectProperty' && path.node.key.type === 'Identifier' && @@ -402,7 +400,7 @@ export class Visitor { } _isParentAroundSelection( - node: babel.types.ArrayExpression | babel.types.CallExpression + node: t.ArrayExpression | t.CallExpression ): boolean { if ( node.loc?.start?.line && @@ -420,7 +418,7 @@ export class Visitor { return false; } - _isObjectPropBeforeSelection(node: babel.types.ObjectProperty): boolean { + _isObjectPropBeforeSelection(node: t.ObjectProperty): boolean { if ( node.key.loc?.end && (node.key.loc?.end.line - 1 < this._selection.start?.line || @@ -433,9 +431,7 @@ export class Visitor { return false; } - _isVariableIdentifierBeforeSelection( - node: babel.types.VariableDeclarator - ): boolean { + _isVariableIdentifierBeforeSelection(node: t.VariableDeclarator): boolean { if ( node.id.loc?.end && (node.id.loc?.end.line - 1 < this._selection.start?.line || @@ -448,9 +444,7 @@ export class Visitor { return false; } - _isWithinSelection( - node: babel.types.ArrayExpression | babel.types.ObjectExpression - ): boolean { + _isWithinSelection(node: t.ArrayExpression | t.ObjectExpression): boolean { if ( node.loc?.start?.line && node.loc.start.line - 1 === this._selection.start?.line && @@ -467,7 +461,7 @@ export class Visitor { return false; } - _checkIsArrayWithinSelection(node: babel.types.Node): void { + _checkIsArrayWithinSelection(node: t.Node): void { if ( node.type === 'ArrayExpression' && this._isWithinSelection(node) && @@ -477,7 +471,7 @@ export class Visitor { } } - _checkIsObjectWithinSelection(node: babel.types.Node): void { + _checkIsObjectWithinSelection(node: t.Node): void { if ( node.type === 'ObjectExpression' && this._isWithinSelection(node) && @@ -487,7 +481,7 @@ export class Visitor { } } - _checkIsBSONSelectionInArray(node: babel.types.Node): void { + _checkIsBSONSelectionInArray(node: t.Node): void { if ( node.type === 'ArrayExpression' && this._isParentAroundSelection(node) @@ -501,7 +495,7 @@ export class Visitor { } } - _checkIsBSONSelectionInFunction(node: babel.types.Node): void { + _checkIsBSONSelectionInFunction(node: t.Node): void { if (node.type === 'CallExpression' && this._isParentAroundSelection(node)) { node.arguments.forEach((item) => { if (item) { @@ -512,7 +506,7 @@ export class Visitor { } } - _checkIsBSONSelectionInVariable(node: babel.types.Node) { + _checkIsBSONSelectionInVariable(node: t.Node) { if ( node.type === 'VariableDeclarator' && node.init && @@ -523,7 +517,7 @@ export class Visitor { } } - _checkIsBSONSelectionInObject(node: babel.types.Node) { + _checkIsBSONSelectionInObject(node: t.Node) { if ( node.type === 'ObjectProperty' && this._isObjectPropBeforeSelection(node) @@ -533,16 +527,14 @@ export class Visitor { } } - _checkIsBSONSelection(node: babel.types.Node): void { + _checkIsBSONSelection(node: t.Node): void { this._checkIsBSONSelectionInFunction(node); this._checkIsBSONSelectionInArray(node); this._checkIsBSONSelectionInVariable(node); this._checkIsBSONSelectionInObject(node); } - _checkIsCollectionNameAsMemberExpression( - node: babel.types.MemberExpression - ): void { + _checkIsCollectionNameAsMemberExpression(node: t.MemberExpression): void { if ( node.object.type === 'Identifier' && node.object.name === 'db' && @@ -556,7 +548,7 @@ export class Visitor { } } - _checkGetCollectionAsSimpleString(node: babel.types.CallExpression): void { + _checkGetCollectionAsSimpleString(node: t.CallExpression): void { if ( node.arguments[0].type === 'StringLiteral' && node.arguments[0].value.includes(PLACEHOLDER) && @@ -566,7 +558,7 @@ export class Visitor { } } - _checkGetCollectionAsTemplate(node: babel.types.CallExpression): void { + _checkGetCollectionAsTemplate(node: t.CallExpression): void { if ( node.arguments[0].type === 'TemplateLiteral' && node.arguments[0].quasis.length === 1 && @@ -577,9 +569,7 @@ export class Visitor { } } - _checkIsCollectionNameAsCallExpression( - node: babel.types.CallExpression - ): void { + _checkIsCollectionNameAsCallExpression(node: t.CallExpression): void { if ( node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && @@ -593,7 +583,7 @@ export class Visitor { } } - _checkHasAggregationCall(node: babel.types.MemberExpression): void { + _checkHasAggregationCall(node: t.MemberExpression): void { if ( node.object.type === 'CallExpression' && node.property.type === 'Identifier' && @@ -608,7 +598,7 @@ export class Visitor { } } - _checkHasFindCall(node: babel.types.MemberExpression): void { + _checkHasFindCall(node: t.MemberExpression): void { if ( node.object.type === 'CallExpression' && node.property.type === 'Identifier' && @@ -623,7 +613,7 @@ export class Visitor { } } - _checkHasDatabaseName(node: babel.types.CallExpression): void { + _checkHasDatabaseName(node: t.CallExpression): void { if ( node.callee.type === 'Identifier' && node.callee.name === 'use' && @@ -639,9 +629,7 @@ export class Visitor { } } - _checkHasCollectionNameMemberExpression( - node: babel.types.MemberExpression - ): void { + _checkHasCollectionNameMemberExpression(node: t.MemberExpression): void { if ( node.object.type === 'MemberExpression' && node.object.object.type === 'Identifier' && @@ -661,7 +649,7 @@ export class Visitor { } } - _checkHasCollectionNameCallExpression(node: babel.types.MemberExpression) { + _checkHasCollectionNameCallExpression(node: t.MemberExpression) { if ( node.object.type === 'CallExpression' && node.object.callee.type === 'MemberExpression' && @@ -677,12 +665,12 @@ export class Visitor { } } - _checkHasCollectionName(node: babel.types.MemberExpression): void { + _checkHasCollectionName(node: t.MemberExpression): void { this._checkHasCollectionNameMemberExpression(node); this._checkHasCollectionNameCallExpression(node); } - _checkIsCollectionMemberExpression(node: babel.types.MemberExpression): void { + _checkIsCollectionMemberExpression(node: t.MemberExpression): void { if ( node.object.type === 'MemberExpression' && node.object.object.type === 'Identifier' && @@ -695,7 +683,7 @@ export class Visitor { } } - _checkIsCollectionCallExpression(node: babel.types.MemberExpression): void { + _checkIsCollectionCallExpression(node: t.MemberExpression): void { if ( node.object.type === 'CallExpression' && node.object.callee.type === 'MemberExpression' && @@ -711,13 +699,13 @@ export class Visitor { } } - _checkIsCollectionSymbol(node: babel.types.MemberExpression): void { + _checkIsCollectionSymbol(node: t.MemberExpression): void { this._checkIsCollectionMemberExpression(node); this._checkIsCollectionCallExpression(node); } _checkIsStreamProcessorNameAsMemberExpression( - node: babel.types.MemberExpression + node: t.MemberExpression ): void { if ( node.object.type === 'Identifier' && @@ -733,9 +721,7 @@ export class Visitor { } } - _checkIsStreamProcessorNameAsCallExpression( - node: babel.types.CallExpression - ): void { + _checkIsStreamProcessorNameAsCallExpression(node: t.CallExpression): void { if ( node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && @@ -749,9 +735,7 @@ export class Visitor { } } - _checkGetStreamProcessorAsSimpleString( - node: babel.types.CallExpression - ): void { + _checkGetStreamProcessorAsSimpleString(node: t.CallExpression): void { if ( node.arguments[0].type === 'StringLiteral' && node.arguments[0].value.includes(PLACEHOLDER) && @@ -761,7 +745,7 @@ export class Visitor { } } - _checkGetStreamProcessorAsTemplate(node: babel.types.CallExpression): void { + _checkGetStreamProcessorAsTemplate(node: t.CallExpression): void { if ( node.arguments[0].type === 'TemplateLiteral' && node.arguments[0].quasis.length === 1 && @@ -772,14 +756,12 @@ export class Visitor { } } - _checkHasStreamProcessorName(node: babel.types.MemberExpression): void { + _checkHasStreamProcessorName(node: t.MemberExpression): void { this._checkHasStreamProcessorNameMemberExpression(node); this._checkHasStreamProcessorNameCallExpression(node); } - _checkHasStreamProcessorNameMemberExpression( - node: babel.types.MemberExpression - ): void { + _checkHasStreamProcessorNameMemberExpression(node: t.MemberExpression): void { if ( node.object.type === 'MemberExpression' && node.object.object.type === 'Identifier' && @@ -799,9 +781,7 @@ export class Visitor { } } - _checkHasStreamProcessorNameCallExpression( - node: babel.types.MemberExpression - ) { + _checkHasStreamProcessorNameCallExpression(node: t.MemberExpression) { if ( node.object.type === 'CallExpression' && node.object.callee.type === 'MemberExpression' && @@ -817,14 +797,12 @@ export class Visitor { } } - _checkIsStreamProcessorSymbol(node: babel.types.MemberExpression): void { + _checkIsStreamProcessorSymbol(node: t.MemberExpression): void { this._checkIsStreamProcessorMemberExpression(node); this._checkIsStreamProcessorCallExpression(node); } - _checkIsStreamProcessorMemberExpression( - node: babel.types.MemberExpression - ): void { + _checkIsStreamProcessorMemberExpression(node: t.MemberExpression): void { if ( node.object.type === 'MemberExpression' && node.object.object.type === 'Identifier' && @@ -837,9 +815,7 @@ export class Visitor { } } - _checkIsStreamProcessorCallExpression( - node: babel.types.MemberExpression - ): void { + _checkIsStreamProcessorCallExpression(node: t.MemberExpression): void { if ( node.object.type === 'CallExpression' && node.object.callee.type === 'MemberExpression' && diff --git a/src/telemetry/connectionTelemetry.ts b/src/telemetry/connectionTelemetry.ts index bbfd3bfb2..c92f675ac 100644 --- a/src/telemetry/connectionTelemetry.ts +++ b/src/telemetry/connectionTelemetry.ts @@ -1,11 +1,9 @@ import type { DataService } from 'mongodb-data-service'; -import { getCloudInfo } from 'mongodb-cloud-info'; import mongoDBBuildInfo from 'mongodb-build-info'; -import resolveMongodbSrv from 'resolve-mongodb-srv'; import { ConnectionTypes } from '../connectionController'; import { createLogger } from '../logging'; -import ConnectionString from 'mongodb-connection-string-url'; +import type { TopologyType } from 'mongodb'; const log = createLogger('connection telemetry helper'); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -14,81 +12,109 @@ const { version } = require('../../package.json'); export type NewConnectionTelemetryEventProperties = { auth_strategy?: string; is_atlas?: boolean; - is_localhost?: boolean; + is_local_atlas?: boolean; + atlas_hostname?: string | null; is_data_lake?: boolean; is_enterprise?: boolean; - is_public_cloud?: boolean; dl_version?: string | null; - public_cloud_name?: string | null; is_genuine?: boolean; non_genuine_server_name?: string | null; server_version?: string; server_arch?: string; - server_os?: string; + server_os_family?: string; is_used_connect_screen?: boolean; is_used_command_palette?: boolean; is_used_saved_connection?: boolean; vscode_mdb_extension_version?: string; -}; + topology_type?: TopologyType; +} & HostInformation; -type CloudInfo = { - isPublicCloud?: boolean; - publicCloudName?: string | null; +export type HostInformation = { + is_localhost?: boolean; + is_atlas_url?: boolean; + is_do_url?: boolean; // Is digital ocean url. + is_public_cloud?: boolean; + public_cloud_name?: string; }; -async function getHostnameForConnection( - connectionStringData: ConnectionString -): Promise { - if (connectionStringData.isSRV) { - const uri = await resolveMongodbSrv(connectionStringData.toString()).catch( - () => null - ); - if (!uri) { - return undefined; - } - connectionStringData = new ConnectionString(uri, { - looseValidation: true, - }); +function getHostnameForConnection(dataService: DataService): string | null { + const lastSeenTopology = dataService.getLastSeenTopology(); + const resolvedHost = lastSeenTopology?.servers.values().next().value.address; + + if (resolvedHost.startsWith('[')) { + return resolvedHost.slice(1).split(']')[0]; // IPv6 } - const [hostname] = (connectionStringData.hosts[0] ?? '').split(':'); - return hostname; + return resolvedHost.split(':')[0]; } -async function getCloudInfoFromDataService( - dataService: DataService -): Promise { - const hostname = await getHostnameForConnection( - dataService.getConnectionString() - ); - const cloudInfo: { - isAws?: boolean; - isAzure?: boolean; - isGcp?: boolean; - } = await getCloudInfo(hostname); - - if (cloudInfo.isAws) { +async function getPublicCloudInfo(host: string): Promise<{ + public_cloud_name?: string; + is_public_cloud?: boolean; +}> { + try { + const { getCloudInfo } = await import('mongodb-cloud-info'); + const { isAws, isAzure, isGcp } = await getCloudInfo(host); + let publicCloudName; + + if (isAws) { + publicCloudName = 'AWS'; + } else if (isAzure) { + publicCloudName = 'Azure'; + } else if (isGcp) { + publicCloudName = 'GCP'; + } + + if (publicCloudName === undefined) { + return { is_public_cloud: false }; + } + return { - isPublicCloud: true, - publicCloudName: 'aws', + is_public_cloud: true, + public_cloud_name: publicCloudName, }; + } catch (err) { + // Cannot resolve dns used by mongodb-cloud-info in the browser environment. + return {}; } - if (cloudInfo.isGcp) { +} + +async function getHostInformation( + host: string | null +): Promise { + if (!host) { return { - isPublicCloud: true, - publicCloudName: 'gcp', + is_do_url: false, + is_atlas_url: false, + is_localhost: false, }; } - if (cloudInfo.isAzure) { + + if (mongoDBBuildInfo.isLocalhost(host)) { return { - isPublicCloud: true, - publicCloudName: 'azure', + is_public_cloud: false, + is_do_url: false, + is_atlas_url: false, + is_localhost: true, }; } + if (mongoDBBuildInfo.isDigitalOcean(host)) { + return { + is_localhost: false, + is_public_cloud: false, + is_atlas_url: false, + is_do_url: true, + }; + } + + const publicCloudInfo = await getPublicCloudInfo(host); + return { - isPublicCloud: false, - publicCloudName: null, + is_localhost: false, + is_do_url: false, + is_atlas_url: mongoDBBuildInfo.isAtlas(host), + ...publicCloudInfo, }; } @@ -109,27 +135,29 @@ export async function getConnectionTelemetryProperties( const authMechanism = connectionString.searchParams.get('authMechanism'); const username = connectionString.username ? 'DEFAULT' : 'NONE'; const authStrategy = authMechanism ?? username; - - const [instance, cloudInfo] = await Promise.all([ - dataService.instance(), - getCloudInfoFromDataService(dataService), - ]); + const resolvedHostname = getHostnameForConnection(dataService); + const { dataLake, genuineMongoDB, host, build, isAtlas, isLocalAtlas } = + await dataService.instance(); + const atlasHostname = isAtlas ? resolvedHostname : null; preparedProperties = { ...preparedProperties, + ...(await getHostInformation( + resolvedHostname || connectionString.toString() + )), auth_strategy: authStrategy, - is_atlas: mongoDBBuildInfo.isAtlas(connectionString.toString()), - is_localhost: mongoDBBuildInfo.isLocalhost(connectionString.toString()), - is_data_lake: instance.dataLake.isDataLake, - is_enterprise: instance.build.isEnterprise, - is_public_cloud: cloudInfo.isPublicCloud, - dl_version: instance.dataLake.version, - public_cloud_name: cloudInfo.publicCloudName, - is_genuine: instance.genuineMongoDB.isGenuine, - non_genuine_server_name: instance.genuineMongoDB.dbType, - server_version: instance.build.version, - server_arch: instance.host.arch, - server_os: instance.host.os, + is_atlas: isAtlas, + atlas_hostname: atlasHostname, + is_local_atlas: isLocalAtlas, + is_data_lake: dataLake.isDataLake, + dl_version: dataLake.version, + is_enterprise: build.isEnterprise, + is_genuine: genuineMongoDB.isGenuine, + non_genuine_server_name: genuineMongoDB.dbType, + server_version: build.version, + server_arch: host.arch, + server_os_family: host.os_family, + topology_type: dataService.getCurrentTopologyType(), }; } catch (error) { log.error('Getting connection telemetry properties failed', error); diff --git a/src/test/suite/telemetry/connectionTelemetry.test.ts b/src/test/suite/telemetry/connectionTelemetry.test.ts index d393cf597..43708a28a 100644 --- a/src/test/suite/telemetry/connectionTelemetry.test.ts +++ b/src/test/suite/telemetry/connectionTelemetry.test.ts @@ -1,114 +1,635 @@ -import { before, after, beforeEach, afterEach } from 'mocha'; +import { before, beforeEach, afterEach } from 'mocha'; import { connect } from 'mongodb-data-service'; import { expect } from 'chai'; import sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; -import * as getCloudInfoModule from 'mongodb-cloud-info'; - import { ConnectionTypes } from '../../../connectionController'; import { getConnectionTelemetryProperties } from '../../../telemetry/connectionTelemetry'; import { TEST_DATABASE_URI } from '../dbTestHelper'; +import ConnectionString from 'mongodb-connection-string-url'; + suite('ConnectionTelemetry Controller Test Suite', function () { suite('with mock data service', function () { this.timeout(8000); - let dataServiceStub: DataService; const sandbox = sinon.createSandbox(); + let dataServiceStub; + let getConnectionStringStub; + let getLastSeenTopology; + let instanceStub; before(() => { - const getConnectionStringStub = sandbox.stub(); - getConnectionStringStub.returns({ - hosts: ['localhost:27088'], - searchParams: { get: () => null }, - username: 'authMechanism', - } as unknown as ReturnType); - - const instanceStub = sandbox.stub(); - instanceStub.resolves({ - dataLake: {}, - build: {}, - genuineMongoDB: {}, - host: {}, - } as unknown as Awaited>); - + getConnectionStringStub = sandbox.stub(); + getLastSeenTopology = sandbox.stub(); + instanceStub = sandbox.stub(); dataServiceStub = { + getCurrentTopologyType: sandbox.stub(), getConnectionString: getConnectionStringStub, + getLastSeenTopology: getLastSeenTopology, instance: instanceStub, } as unknown as DataService; + }); + + afterEach(() => { + sandbox.restore(); + }); - sandbox.stub(getCloudInfoModule, 'getCloudInfo').callsFake(() => - Promise.resolve({ - isAws: false, - isGcp: false, - isAzure: false, - }) + test('it tracks public cloud info', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://13.64.151.161') ); + getLastSeenTopology.returns({ + servers: new Map().set('13.64.151.161', { + address: '13.64.151.161', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_FORM + ); + expect(instanceTelemetry.is_public_cloud).to.equal(true); + expect(instanceTelemetry.public_cloud_name).to.equal('Azure'); }); - after(() => { - sandbox.restore(); + test('it tracks non public cloud info', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://localhost:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_FORM + ); + expect(instanceTelemetry.is_public_cloud).to.equal(false); }); - test('it returns is_used_connect_screen true when the connection type is form', async () => { + test('it tracks atlas local dev', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + isAtlas: false, + isLocalAtlas: true, + featureCompatibilityVersion: null, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://localhost:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + const instanceTelemetry = await getConnectionTelemetryProperties( dataServiceStub, ConnectionTypes.CONNECTION_FORM ); + expect(instanceTelemetry.is_atlas).to.equal(false); + expect(instanceTelemetry.atlas_hostname).to.equal(null); + expect(instanceTelemetry.is_atlas_url).to.equal(false); + expect(instanceTelemetry.is_local_atlas).to.equal(true); + }); + + test('it tracks atlas', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + isAtlas: true, + isLocalAtlas: false, + featureCompatibilityVersion: null, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://test-data-sets-a011bb.mongodb.net') + ); + getLastSeenTopology.returns({ + servers: new Map().set('test-data-sets-00-02-a011bb.mongodb.net', { + address: 'test-data-sets-00-02-a011bb.mongodb.net', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_FORM + ); + expect(instanceTelemetry.is_atlas).to.equal(true); + expect(instanceTelemetry.atlas_hostname).to.equal( + 'test-data-sets-00-02-a011bb.mongodb.net' + ); + expect(instanceTelemetry.is_atlas_url).to.equal(true); + expect(instanceTelemetry.is_local_atlas).to.equal(false); + }); + + test('it tracks atlas IPv6', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + isAtlas: true, + isLocalAtlas: false, + featureCompatibilityVersion: null, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://[3fff:0:a88:15a3::ac2f]:8001') + ); + getLastSeenTopology.returns({ + servers: new Map().set('[3fff:0:a88:15a3::ac2f]:8001', { + address: '[3fff:0:a88:15a3::ac2f]:8001', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_FORM + ); + expect(instanceTelemetry.is_atlas).to.equal(true); + expect(instanceTelemetry.atlas_hostname).to.equal( + '3fff:0:a88:15a3::ac2f' + ); + expect(instanceTelemetry.is_atlas_url).to.equal(false); + }); + + test('it tracks atlas with fallback to original uri if failed resolving srv', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + isAtlas: false, + isLocalAtlas: false, + featureCompatibilityVersion: null, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://localhost') + ); + getLastSeenTopology.returns({ + servers: new Map().set('', { + address: '', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_FORM + ); + expect(instanceTelemetry.is_localhost).to.equal(true); + }); + + test('it tracks digital ocean', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://example.mongo.ondigitalocean.com:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('example.mongo.ondigitalocean.com:27017', { + address: 'example.mongo.ondigitalocean.com:27017', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_STRING + ); + expect(instanceTelemetry.is_localhost).to.equal(false); + expect(instanceTelemetry.is_atlas_url).to.equal(false); + expect(instanceTelemetry.is_do_url).to.equal(true); + expect(instanceTelemetry.is_genuine).to.equal(true); + }); + + test('it tracks is_used_connect_screen true when the connection type is form', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://localhost:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_FORM + ); expect(instanceTelemetry.is_used_connect_screen).to.equal(true); expect(instanceTelemetry.is_used_command_palette).to.equal(false); expect(instanceTelemetry.is_used_saved_connection).to.equal(false); }); - test('it returns is_used_command_palette true when the connection type is string', async () => { + test('it tracks is_used_command_palette true when the connection type is string', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://localhost:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + const instanceTelemetry = await getConnectionTelemetryProperties( dataServiceStub, ConnectionTypes.CONNECTION_STRING ); - expect(instanceTelemetry.is_used_connect_screen).to.equal(false); expect(instanceTelemetry.is_used_command_palette).to.equal(true); expect(instanceTelemetry.is_used_saved_connection).to.equal(false); }); - test('it returns is_used_saved_connection true when the connection type is id', async () => { + test('it tracks is_used_saved_connection true when the connection type is id', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://localhost:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + const instanceTelemetry = await getConnectionTelemetryProperties( dataServiceStub, ConnectionTypes.CONNECTION_ID ); - expect(instanceTelemetry.is_used_connect_screen).to.equal(false); expect(instanceTelemetry.is_used_command_palette).to.equal(false); expect(instanceTelemetry.is_used_saved_connection).to.equal(true); }); - test('it has is_localhost false for a remote connection', async () => { + test('it tracks is_localhost false for a remote connection', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://example.mongo.ondigitalocean.com:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('example.mongo.ondigitalocean.com:27017', { + address: 'example.mongo.ondigitalocean.com:27017', + }), + }); + const instanceTelemetry = await getConnectionTelemetryProperties( dataServiceStub, ConnectionTypes.CONNECTION_STRING ); - expect(instanceTelemetry.is_localhost).to.equal(false); }); - test('it has a default is atlas false', async () => { + test('it tracks is_localhost true for a local connection', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://localhost:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + const instanceTelemetry = await getConnectionTelemetryProperties( dataServiceStub, ConnectionTypes.CONNECTION_STRING ); + expect(instanceTelemetry.is_localhost).to.equal(true); + }); - expect(instanceTelemetry.is_atlas).to.equal(false); + test('it tracks server info for ubuntu', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: '1.2.3', + }, + genuineMongoDB: { + dbType: 'mongo_2', + isGenuine: true, + }, + host: { + arch: 'debian', + os_family: 'ubuntu', + }, + build: { + isEnterprise: false, + version: '4.3.9', + }, + isAtlas: false, + isLocalAtlas: false, + featureCompatibilityVersion: null, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://127.0.0.1') + ); + getLastSeenTopology.returns({ + servers: new Map().set('127.0.0.1', { + address: '127.0.0.1', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_STRING + ); + expect(instanceTelemetry.server_version).to.equal('4.3.9'); + expect(instanceTelemetry.server_arch).to.equal('debian'); + expect(instanceTelemetry.server_os_family).to.equal('ubuntu'); }); - test('it has a default driver auth mechanism undefined', async () => { + test('it tracks server info for mac', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: true, + version: '1.2.3', + }, + genuineMongoDB: { + dbType: 'mongo', + isGenuine: false, + }, + host: { + arch: 'darwin', + os_family: 'mac', + }, + build: { + isEnterprise: true, + version: '4.3.2', + }, + isAtlas: false, + isLocalAtlas: false, + featureCompatibilityVersion: null, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://127.0.0.1') + ); + getLastSeenTopology.returns({ + servers: new Map().set('127.0.0.1', { + address: '127.0.0.1', + }), + }); + const instanceTelemetry = await getConnectionTelemetryProperties( dataServiceStub, ConnectionTypes.CONNECTION_STRING ); + expect(instanceTelemetry.server_version).to.equal('4.3.2'); + expect(instanceTelemetry.server_arch).to.equal('darwin'); + expect(instanceTelemetry.server_os_family).to.equal('mac'); + }); + + test('it returns DEFAULT when auth mechanism undefined and username is specified', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://artishok:pass@localhost:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_STRING + ); expect(instanceTelemetry.auth_strategy).to.equal('DEFAULT'); }); + + test('it returns NONE when auth mechanism undefined and username undefined', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString('mongodb://localhost:27017') + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_STRING + ); + expect(instanceTelemetry.auth_strategy).to.equal('NONE'); + }); + + test('it returns authMechanism when specified', async () => { + instanceStub.resolves({ + dataLake: { + isDataLake: false, + version: 'na', + }, + genuineMongoDB: { + dbType: 'na', + isGenuine: true, + }, + host: {}, + build: { + isEnterprise: false, + version: 'na', + }, + }); + getConnectionStringStub.returns( + new ConnectionString( + 'mongodb://foo:bar@localhost:27017/?authSource=source&authMechanism=SCRAM-SHA-1' + ) + ); + getLastSeenTopology.returns({ + servers: new Map().set('localhost:27017', { + address: 'localhost:27017', + }), + }); + + const instanceTelemetry = await getConnectionTelemetryProperties( + dataServiceStub, + ConnectionTypes.CONNECTION_STRING + ); + expect(instanceTelemetry.auth_strategy).to.equal('SCRAM-SHA-1'); + }); }); // TODO: Enable test back when Insider is fixed https://jira.mongodb.org/browse/VSCODE-452 diff --git a/webpack.config.js b/webpack.config.js index 3fca92511..98b418c8c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,13 +2,13 @@ const path = require('path'); const webpack = require('webpack'); -const autoprefixer = require('autoprefixer'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin'); const { merge } = require('webpack-merge'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const { WebpackDependenciesPlugin } = require('@mongodb-js/sbom-tools'); +const TerserPlugin = require('terser-webpack-plugin'); module.exports = (env, argv) => { const outputPath = path.join(__dirname, 'dist'); @@ -43,6 +43,10 @@ module.exports = (env, argv) => { // We don't currently support kerberos in our extension. kerberos: false, + + // Optional native-addon dependencies of ssh2 + 'cpu-features': false, + './crypto/build/Release/sshcrypto.node': false, }, }, @@ -67,6 +71,24 @@ module.exports = (env, argv) => { 'os-dns-native': 'os-dns-native', 'mongodb-client-encryption': 'mongodb-client-encryption', 'compass-preferences-model': 'compass-preferences-model', + '@mongodb-js/zstd': '@mongodb-js/zstd', + 'gcp-metadata': 'gcp-metadata', + encoding: 'encoding', + }, + optimization: { + minimizer: [ + new TerserPlugin({ + terserOptions: { + output: { ascii_only: true }, + // Not keeping classnames breaks shell-api during minification + keep_classnames: true, + compress: { + // The 'bindings' package relies on `error.stack;` having side effects. + pure_getters: false, + }, + }, + }), + ], }, plugins: [ webpackDependenciesPlugin, @@ -132,22 +154,12 @@ module.exports = (env, argv) => { entry: { languageServer: './src/language/server.ts', }, - optimization: { - // Don't minimize in order to preserve - // the signature names from @mongosh/shell-api. - minimize: false, - }, }); const languageServerWorkerConfig = merge(nodeTargetConfig(), { entry: { languageServerWorker: './src/language/worker.ts', }, - optimization: { - // Don't minimize in order to preserve - // the signature names from @mongosh/shell-api. - minimize: false, - }, }); const webviewConfig = merge(baseConfig(), { @@ -189,6 +201,12 @@ module.exports = (env, argv) => { process: 'process/browser', }), ], + watchOptions: { + // For some systems, watching many files can result in a lot of CPU or memory usage + // https://webpack.js.org/configuration/watch/#watchoptionsignored + // don't use this pattern, if you have a monorepo with linked packages. + ignored: /node_modules/, + }, }); return [ From da9bc438605238c4036791bc03d8a42f411f81a2 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 12 Aug 2024 13:13:39 +0200 Subject: [PATCH 03/45] feat: open pre-populated with MongoDB info issue reporter VSCODE-584 (#768) --- package.json | 8 ++++ src/commands/index.ts | 2 + src/mdbExtensionController.ts | 13 ++++++ src/participant/participant.ts | 77 ++++++++++++++++++---------------- 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 9163a2df3..1b2cf3ffa 100644 --- a/package.json +++ b/package.json @@ -187,6 +187,10 @@ "command": "mdb.openOverviewPage", "title": "MongoDB: Open Overview Page" }, + { + "command": "mdb.openMongoDBIssueReporter", + "title": "MongoDB: Open MongoDB Issue Reporter" + }, { "command": "mdb.openMongoDBShell", "title": "MongoDB: Launch MongoDB Shell" @@ -789,6 +793,10 @@ "command": "mdb.createNewPlaygroundFromOverviewPage", "when": "false" }, + { + "command": "mdb.openMongoDBIssueReporter", + "when": "true" + }, { "command": "mdb.createNewPlaygroundFromTreeView", "when": "false" diff --git a/src/commands/index.ts b/src/commands/index.ts index 61513dc2c..ba4c32cde 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -5,6 +5,8 @@ enum EXTENSION_COMMANDS { MDB_DISCONNECT = 'mdb.disconnect', MDB_REMOVE_CONNECTION = 'mdb.removeConnection', + OPEN_MONGODB_ISSUE_REPORTER = 'mdb.openMongoDBIssueReporter', + MDB_OPEN_MDB_SHELL = 'mdb.openMongoDBShell', MDB_OPEN_MDB_SHELL_FROM_TREE_VIEW = 'mdb.treeViewOpenMongoDBShell', diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 579757dac..fb6d51d39 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -183,6 +183,19 @@ export default class MDBExtensionController implements vscode.Disposable { this._connectionController.changeActiveConnection() ); + this.registerCommand( + EXTENSION_COMMANDS.OPEN_MONGODB_ISSUE_REPORTER, + async () => { + return await vscode.commands.executeCommand( + 'workbench.action.openIssueReporter', + { + extensionId: 'mongodb.mongodb-vscode', + issueSource: 'extension', + } + ); + } + ); + // ------ SHELL ------ // this.registerCommand(EXTENSION_COMMANDS.MDB_OPEN_MDB_SHELL, () => launchMongoShell(this._connectionController) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index d49e58cf0..3b4b527da 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -18,21 +18,7 @@ interface ChatResult extends vscode.ChatResult { } export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; -export const CHAT_PARTICIPANT_MODEL = 'gpt-4'; - -function handleEmptyQueryRequest(participantId?: string): ChatResult { - log.info('Chat request participant id', participantId); - - return { - metadata: { - command: '', - }, - errorDetails: { - message: - 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".', - }, - }; -} +export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; export function getRunnableContentFromString(responseContent: string) { const matchedJSQueryContent = responseContent.match( @@ -75,6 +61,9 @@ export class ParticipantController { 'images', 'mongodb.png' ); + log.info('Chat Participant Created', { + participantId: this._participant?.id, + }); return this._participant; } @@ -82,6 +71,18 @@ export class ParticipantController { return this._participant || this.createParticipant(context); } + handleEmptyQueryRequest(): ChatResult { + return { + metadata: { + command: '', + }, + errorDetails: { + message: + 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".', + }, + }; + } + handleError(err: any, stream: vscode.ChatResponseStream): void { // Making the chat request might fail because // - model does not exist @@ -99,6 +100,9 @@ export class ParticipantController { ) ); } + } else { + // Re-throw other errors so they show up in the UI. + throw err; } } @@ -151,7 +155,7 @@ export class ParticipantController { and you are very good at it. The user will provide the basis for the query. Keep your response concise. Respond with markdown, code snippets are possible with '''javascript. You can imagine the schema, collection, and database name. - Respond in MongoDB shell syntax using the '''javascript code style.'.`), + Respond in MongoDB shell syntax using the '''javascript code style.`), ]; context.history.map((historyItem) => { @@ -189,29 +193,30 @@ export class ParticipantController { stream, token, }); + const queryContent = getRunnableContentFromString(responseContent); - if (!queryContent || queryContent.trim().length === 0) { - return { metadata: { command: '' } }; - } + if (queryContent && queryContent.trim().length) { + stream.button({ + command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, + title: vscode.l10n.t('▶️ Run'), + }); - stream.button({ - command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, - title: vscode.l10n.t('▶️ Run'), - }); + stream.button({ + command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + title: vscode.l10n.t('Open in playground'), + }); - stream.button({ - command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, - title: vscode.l10n.t('Open in playground'), - }); + return { + metadata: { + command: '', + stream, + queryContent, + }, + }; + } - return { - metadata: { - command: '', - stream, - queryContent, - }, - }; + return { metadata: { command: '' } }; } // @MongoDB /query find all documents where the "address" has the word Broadway in it. @@ -226,7 +231,7 @@ export class ParticipantController { token: vscode.CancellationToken; }) { if (!request.prompt || request.prompt.trim().length === 0) { - return handleEmptyQueryRequest(this._participant?.id); + return this.handleEmptyQueryRequest(); } let dataService = this._connectionController.getActiveDataService(); @@ -318,8 +323,6 @@ export class ParticipantController { // TODO: Implement this. } else if (request.command === 'schema') { // TODO: Implement this. - } else if (request.command === 'logs') { - // TODO: Implement this. } return await this.handleGenericRequest({ From 60008e750600dc83ad13050188dad604b8bbe8ee Mon Sep 17 00:00:00 2001 From: Rhys Date: Wed, 14 Aug 2024 12:36:41 -0400 Subject: [PATCH 04/45] chore(chat-participant): move prompts to prompts folder (#777) --- src/participant/constants.ts | 1 + src/participant/participant.ts | 63 +++++++----------------------- src/participant/prompts/generic.ts | 41 +++++++++++++++++++ src/participant/prompts/history.ts | 35 +++++++++++++++++ src/participant/prompts/query.ts | 41 +++++++++++++++++++ 5 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 src/participant/constants.ts create mode 100644 src/participant/prompts/generic.ts create mode 100644 src/participant/prompts/history.ts create mode 100644 src/participant/prompts/query.ts diff --git a/src/participant/constants.ts b/src/participant/constants.ts new file mode 100644 index 000000000..e4aac61e0 --- /dev/null +++ b/src/participant/constants.ts @@ -0,0 +1 @@ +export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 3b4b527da..44992c24b 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -3,6 +3,9 @@ import * as vscode from 'vscode'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; import EXTENSION_COMMANDS from '../commands'; +import { GenericPrompt } from './prompts/generic'; +import { CHAT_PARTICIPANT_ID } from './constants'; +import { QueryPrompt } from './prompts/query'; const log = createLogger('participant'); @@ -17,7 +20,6 @@ interface ChatResult extends vscode.ChatResult { stream?: vscode.ChatResponseStream; } -export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; export function getRunnableContentFromString(responseContent: string) { @@ -148,41 +150,11 @@ export class ParticipantController { stream: vscode.ChatResponseStream; token: vscode.CancellationToken; }) { - const messages = [ - // eslint-disable-next-line new-cap - vscode.LanguageModelChatMessage.Assistant(`You are a MongoDB expert! - You create MongoDB queries and aggregation pipelines, - and you are very good at it. The user will provide the basis for the query. - Keep your response concise. Respond with markdown, code snippets are possible with '''javascript. - You can imagine the schema, collection, and database name. - Respond in MongoDB shell syntax using the '''javascript code style.`), - ]; - - context.history.map((historyItem) => { - if ( - historyItem.participant === CHAT_PARTICIPANT_ID && - historyItem instanceof vscode.ChatRequestTurn - ) { - // eslint-disable-next-line new-cap - messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt)); - } - - if ( - historyItem.participant === CHAT_PARTICIPANT_ID && - historyItem instanceof vscode.ChatResponseTurn - ) { - let res = ''; - for (const fragment of historyItem.response) { - res += fragment; - } - // eslint-disable-next-line new-cap - messages.push(vscode.LanguageModelChatMessage.Assistant(res)); - } + const messages = GenericPrompt.buildMessages({ + request, + context, }); - // eslint-disable-next-line new-cap - messages.push(vscode.LanguageModelChatMessage.User(request.prompt)); - const abortController = new AbortController(); token.onCancellationRequested(() => { abortController.abort(); @@ -222,11 +194,12 @@ export class ParticipantController { // @MongoDB /query find all documents where the "address" has the word Broadway in it. async handleQueryRequest({ request, + context, stream, token, }: { request: vscode.ChatRequest; - context?: vscode.ChatContext; + context: vscode.ChatContext; stream: vscode.ChatResponseStream; token: vscode.CancellationToken; }) { @@ -265,17 +238,11 @@ export class ParticipantController { abortController.abort(); }); - const messages = [ - // eslint-disable-next-line new-cap - vscode.LanguageModelChatMessage.Assistant(`You are a MongoDB expert! - You create MongoDB queries and aggregation pipelines, - and you are very good at it. The user will provide the basis for the query. - Keep your response concise. Respond with markdown, code snippets are possible with '''javascript. - You can imagine the schema, collection, and database name. - Respond in MongoDB shell syntax using the '''javascript code style.`), - // eslint-disable-next-line new-cap - vscode.LanguageModelChatMessage.User(request.prompt), - ]; + const messages = QueryPrompt.buildMessages({ + context, + request, + }); + const responseContent = await this.getChatResponseContent({ messages, stream, @@ -320,9 +287,9 @@ export class ParticipantController { }); return this._chatResult; } else if (request.command === 'docs') { - // TODO: Implement this. + // TODO(VSCODE-570): Implement this. } else if (request.command === 'schema') { - // TODO: Implement this. + // TODO(VSCODE-571): Implement this. } return await this.handleGenericRequest({ diff --git a/src/participant/prompts/generic.ts b/src/participant/prompts/generic.ts new file mode 100644 index 000000000..bde4604f0 --- /dev/null +++ b/src/participant/prompts/generic.ts @@ -0,0 +1,41 @@ +import * as vscode from 'vscode'; + +import { getHistoryMessages } from './history'; + +export class GenericPrompt { + static getSystemPrompt(): vscode.LanguageModelChatMessage { + const prompt = `You are a MongoDB expert. +Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. +Keep your response concise. +You should suggest queries that are performant and correct. +Respond with markdown, suggest code in a Markdown code block that begins with \'\'\'javascript and ends with \`\`\`. +You can imagine the schema, collection, and database name. +Respond in MongoDB shell syntax using the \'\'\'javascript code block syntax.`; + + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.Assistant(prompt); + } + + static getUserPrompt( + request: vscode.ChatRequest + ): vscode.LanguageModelChatMessage { + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.User(request.prompt); + } + + static buildMessages({ + context, + request, + }: { + request: vscode.ChatRequest; + context: vscode.ChatContext; + }): vscode.LanguageModelChatMessage[] { + const messages = [ + GenericPrompt.getSystemPrompt(), + ...getHistoryMessages({ context }), + GenericPrompt.getUserPrompt(request), + ]; + + return messages; + } +} diff --git a/src/participant/prompts/history.ts b/src/participant/prompts/history.ts new file mode 100644 index 000000000..907838b7e --- /dev/null +++ b/src/participant/prompts/history.ts @@ -0,0 +1,35 @@ +import * as vscode from 'vscode'; + +import { CHAT_PARTICIPANT_ID } from '../constants'; + +export function getHistoryMessages({ + context, +}: { + context: vscode.ChatContext; +}): vscode.LanguageModelChatMessage[] { + const messages: vscode.LanguageModelChatMessage[] = []; + + context.history.map((historyItem) => { + if ( + historyItem.participant === CHAT_PARTICIPANT_ID && + historyItem instanceof vscode.ChatRequestTurn + ) { + // eslint-disable-next-line new-cap + messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt)); + } + + if ( + historyItem.participant === CHAT_PARTICIPANT_ID && + historyItem instanceof vscode.ChatResponseTurn + ) { + let res = ''; + for (const fragment of historyItem.response) { + res += fragment; + } + // eslint-disable-next-line new-cap + messages.push(vscode.LanguageModelChatMessage.Assistant(res)); + } + }); + + return messages; +} diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts new file mode 100644 index 000000000..7b27ce2de --- /dev/null +++ b/src/participant/prompts/query.ts @@ -0,0 +1,41 @@ +import * as vscode from 'vscode'; + +import { getHistoryMessages } from './history'; + +export class QueryPrompt { + static getSystemPrompt(): vscode.LanguageModelChatMessage { + const prompt = `You are a MongoDB expert. +Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. +Keep your response concise. +You should suggest queries that are performant and correct. +Respond with markdown, suggest code in a Markdown code block that begins with \'\'\'javascript and ends with \`\`\`. +You can imagine the schema, collection, and database name. +Respond in MongoDB shell syntax using the \'\'\'javascript code block syntax.`; + + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.Assistant(prompt); + } + + static getUserPrompt( + request: vscode.ChatRequest + ): vscode.LanguageModelChatMessage { + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.User(request.prompt); + } + + static buildMessages({ + context, + request, + }: { + request: vscode.ChatRequest; + context: vscode.ChatContext; + }): vscode.LanguageModelChatMessage[] { + const messages = [ + QueryPrompt.getSystemPrompt(), + ...getHistoryMessages({ context }), + QueryPrompt.getUserPrompt(request), + ]; + + return messages; + } +} From e8c64eebbad5795d5d397894e050063c0c2488c8 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Thu, 15 Aug 2024 14:19:22 +0200 Subject: [PATCH 05/45] feat: opt-in via the initial chat participant message VSCODE-568 (#775) * feat: opt-in via the initial chat participant message VSCODE-568 * test: increase connection form timeout * test: increase max_old_space_siz for test task --- .../actions/test-and-build/action.yaml | 2 ++ src/mdbExtensionController.ts | 1 + src/participant/participant.ts | 23 +++++++++++++++++++ src/storage/storageController.ts | 2 ++ .../editors/playgroundController.test.ts | 1 + ...aygroundSelectedCodeActionProvider.test.ts | 1 + .../language/languageServerController.test.ts | 1 + .../views/webview-app/overview-page.test.tsx | 2 +- 8 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/actions/test-and-build/action.yaml b/.github/workflows/actions/test-and-build/action.yaml index 2b9438281..85703a1ea 100644 --- a/.github/workflows/actions/test-and-build/action.yaml +++ b/.github/workflows/actions/test-and-build/action.yaml @@ -70,6 +70,8 @@ runs: shell: bash - name: Run Tests + env: + NODE_OPTIONS: "--max_old_space_size=4096" run: | npm run test shell: bash diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index fb6d51d39..cc1380f5b 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -109,6 +109,7 @@ export default class MDBExtensionController implements vscode.Disposable { new PlaygroundDiagnosticsCodeActionProvider(); this._participantController = new ParticipantController({ connectionController: this._connectionController, + storageController: this._storageController, }); this._playgroundController = new PlaygroundController({ connectionController: this._connectionController, diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 44992c24b..ded24c582 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -3,6 +3,8 @@ import * as vscode from 'vscode'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; import EXTENSION_COMMANDS from '../commands'; +import type { StorageController } from '../storage'; +import { StorageVariables } from '../storage'; import { GenericPrompt } from './prompts/generic'; import { CHAT_PARTICIPANT_ID } from './constants'; import { QueryPrompt } from './prompts/query'; @@ -40,14 +42,18 @@ export class ParticipantController { _participant?: vscode.ChatParticipant; _chatResult: ChatResult; _connectionController: ConnectionController; + _storageController: StorageController; constructor({ connectionController, + storageController, }: { connectionController: ConnectionController; + storageController: StorageController; }) { this._chatResult = { metadata: { command: '' } }; this._connectionController = connectionController; + this._storageController = storageController; } createParticipant(context: vscode.ExtensionContext) { @@ -278,6 +284,23 @@ export class ParticipantController { stream: vscode.ChatResponseStream, token: vscode.CancellationToken ): Promise { + const hasBeenShownWelcomeMessageAlready = !!this._storageController.get( + StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE + ); + + if (!hasBeenShownWelcomeMessageAlready) { + stream.markdown( + vscode.l10n.t(` + Welcome to MongoDB Participant!\n\n + Interact with your MongoDB clusters and generate MongoDB-related code more efficiently with intelligent AI-powered feature, available today in the MongoDB extension.\n\n + Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more information.`) + ); + void this._storageController.update( + StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE, + true + ); + } + if (request.command === 'query') { this._chatResult = await this.handleQueryRequest({ request, diff --git a/src/storage/storageController.ts b/src/storage/storageController.ts index 3ae1a71ec..7a2ba780b 100644 --- a/src/storage/storageController.ts +++ b/src/storage/storageController.ts @@ -12,6 +12,7 @@ export enum StorageVariables { GLOBAL_ANONYMOUS_ID = 'GLOBAL_ANONYMOUS_ID', // Only exists on workspaceState. WORKSPACE_SAVED_CONNECTIONS = 'WORKSPACE_SAVED_CONNECTIONS', + COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE = 'COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE', } // Typically variables default to 'GLOBAL' scope. @@ -52,6 +53,7 @@ interface StorageVariableContents { [StorageVariables.GLOBAL_HAS_BEEN_SHOWN_INITIAL_VIEW]: boolean; [StorageVariables.GLOBAL_SAVED_CONNECTIONS]: ConnectionsFromStorage; [StorageVariables.WORKSPACE_SAVED_CONNECTIONS]: ConnectionsFromStorage; + [StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE]: boolean; } type StoredVariableName = keyof StorageVariableContents; type StoredItem = StorageVariableContents[T]; diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index 66af1b06b..0526aeb34 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -92,6 +92,7 @@ suite('Playground Controller Test Suite', function () { ); testParticipantController = new ParticipantController({ connectionController: testConnectionController, + storageController: testStorageController, }); testPlaygroundController = new PlaygroundController({ connectionController: testConnectionController, diff --git a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts index 74f143249..07f9687d6 100644 --- a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts +++ b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts @@ -59,6 +59,7 @@ suite('Playground Selected CodeAction Provider Test Suite', function () { testParticipantController = new ParticipantController({ connectionController: testConnectionController, + storageController: testStorageController, }); sandbox.replace( diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index 52a660a37..3143f9c1b 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -71,6 +71,7 @@ suite('Language Server Controller Test Suite', () => { ); testParticipantController = new ParticipantController({ connectionController: testConnectionController, + storageController: testStorageController, }); testPlaygroundController = new PlaygroundController({ connectionController: testConnectionController, diff --git a/src/test/suite/views/webview-app/overview-page.test.tsx b/src/test/suite/views/webview-app/overview-page.test.tsx index 27b9b2c05..3dc9abbbf 100644 --- a/src/test/suite/views/webview-app/overview-page.test.tsx +++ b/src/test/suite/views/webview-app/overview-page.test.tsx @@ -39,7 +39,7 @@ describe('OverviewPage test suite', function () { describe('Connection Form', function () { // Rendering the connection form takes ~4 seconds, so we need to increase the timeout. // Not sure on the cause of this slowdown, it could be animation based. - this.timeout(10000); + this.timeout(20000); it('is able to open and close the new connection form', async function () { render(); From 563a8ce42c65f93955edbe4b5f8dc1912f5cd0e1 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Fri, 16 Aug 2024 20:26:03 +0200 Subject: [PATCH 06/45] feat: identify a namespace to place in generated query VSCODE-579 (#778) * feat: identify a namespace to place in generated query VSCODE-579 * feat: parse user prompt * feat: show names to copy * fat: clickable markdown links * feat: allow change connections * docs: add comments * refactor: revert some changes * feat: set isPartialQuery * refactor: remove auxiliary commands * refactor: organise code * docs: add comments * refactor: rename * refactor: remove empty connections list check * refactor: clean up * refactor: add return types * refactor: add todo * refactor: address pr comments * refactor: add new line after faq * docs: link the jira ticket in todos * refactor: address rest of the comments * fix: VSCODE-591 --- package.json | 34 +- src/commands/index.ts | 3 + src/connectionController.ts | 19 +- src/editors/mongoDBDocumentService.ts | 12 +- src/mdbExtensionController.ts | 20 +- src/participant/participant.ts | 509 +++++++++++++++++++------- src/participant/prompts/namespace.ts | 41 +++ src/participant/prompts/query.ts | 47 ++- 8 files changed, 542 insertions(+), 143 deletions(-) create mode 100644 src/participant/prompts/namespace.ts diff --git a/package.json b/package.json index 4ed04d2ae..c385b4e1d 100644 --- a/package.json +++ b/package.json @@ -87,9 +87,11 @@ "id": "mongodb.participant", "name": "MongoDB", "description": "Ask anything about MongoDB, from writing queries to questions about your cluster.", + "isSticky": true, "commands": [ { "name": "query", + "isSticky": true, "description": "Ask how to write MongoDB queries or pipelines. For example, you can ask: \"Show me all the documents where the address contains the word street\"." } ] @@ -161,13 +163,25 @@ } ], "commands": [ + { + "command": "mdb.selectDatabaseWithParticipant", + "title": "MongoDB: Select Database with Participant" + }, + { + "command": "mdb.selectCollectionWithParticipant", + "title": "MongoDB: Select Collection with Participant" + }, + { + "command": "mdb.connectWithParticipant", + "title": "MongoDB: Change Active Connection with Participant" + }, { "command": "mdb.runParticipantQuery", - "title": "Run Content Generated by the Chat Participant" + "title": "Run Content Generated by Participant" }, { "command": "mdb.openParticipantQueryInPlayground", - "title": "Open Generated by the Chat Participant Content In Playground" + "title": "Open Generated by Participant Content In Playground" }, { "command": "mdb.connect", @@ -719,6 +733,22 @@ } ], "commandPalette": [ + { + "command": "mdb.selectDatabaseWithParticipant", + "when": "false" + }, + { + "command": "mdb.selectCollectionWithParticipant", + "when": "false" + }, + { + "command": "mdb.connectWithParticipant", + "when": "false" + }, + { + "command": "mdb.runParticipantQuery", + "when": "false" + }, { "command": "mdb.openParticipantQueryInPlayground", "when": "false" diff --git a/src/commands/index.ts b/src/commands/index.ts index ba4c32cde..2a11bf67e 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -79,6 +79,9 @@ enum EXTENSION_COMMANDS { // Chat participant. OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND = 'mdb.openParticipantQueryInPlayground', RUN_PARTICIPANT_QUERY = 'mdb.runParticipantQuery', + CONNECT_WITH_PARTICIPANT = 'mdb.connectWithParticipant', + SELECT_DATABASE_WITH_PARTICIPANT = 'mdb.selectDatabaseWithParticipant', + SELECT_COLLECTION_WITH_PARTICIPANT = 'mdb.selectCollectionWithParticipant', } export default EXTENSION_COMMANDS; diff --git a/src/connectionController.ts b/src/connectionController.ts index 4b4515b8a..b25da9201 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -419,7 +419,14 @@ export default class ConnectionController { } log.info('Successfully connected', { connectionId }); - void vscode.window.showInformationMessage('MongoDB connection successful.'); + + const message = 'MongoDB connection successful.'; + this._statusView.showMessage(message); + setTimeout(() => { + if (this._statusView._statusBarItem.text === message) { + this._statusView.hideMessage(); + } + }, 5000); dataService.addReauthenticationHandler( this._reauthenticationHandler.bind(this) @@ -603,10 +610,16 @@ export default class ConnectionController { 'mdb.isAtlasStreams', false ); - void vscode.window.showInformationMessage('MongoDB disconnected.'); this._disconnecting = false; - this._statusView.hideMessage(); + + const message = 'MongoDB disconnected.'; + this._statusView.showMessage(message); + setTimeout(() => { + if (this._statusView._statusBarItem.text === message) { + this._statusView.hideMessage(); + } + }, 5000); return true; } diff --git a/src/editors/mongoDBDocumentService.ts b/src/editors/mongoDBDocumentService.ts index 928d02de0..1806cddda 100644 --- a/src/editors/mongoDBDocumentService.ts +++ b/src/editors/mongoDBDocumentService.ts @@ -98,13 +98,11 @@ export default class MongoDBDocumentService { returnDocument: 'after', } ); - - this._statusView.hideMessage(); this._telemetryService.trackDocumentUpdated(source, true); } catch (error) { - this._statusView.hideMessage(); - return this._saveDocumentFailed(formatError(error).message); + } finally { + this._statusView.hideMessage(); } } @@ -141,17 +139,15 @@ export default class MongoDBDocumentService { { limit: 1 } ); - this._statusView.hideMessage(); - if (!documents || documents.length === 0) { return; } return getEJSON(documents[0]); } catch (error) { - this._statusView.hideMessage(); - return this._fetchDocumentFailed(formatError(error).message); + } finally { + this._statusView.hideMessage(); } } } diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index cc1380f5b..008d46002 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -292,7 +292,8 @@ export default class MDBExtensionController implements vscode.Disposable { () => { return this._playgroundController.createPlaygroundFromParticipantQuery({ text: - this._participantController._chatResult.metadata.queryContent || '', + this._participantController._chatResult?.metadata + ?.responseContent || '', }); } ); @@ -301,10 +302,25 @@ export default class MDBExtensionController implements vscode.Disposable { () => { return this._playgroundController.evaluateParticipantQuery({ text: - this._participantController._chatResult.metadata.queryContent || '', + this._participantController._chatResult?.metadata + ?.responseContent || '', }); } ); + this.registerCommand( + EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, + (id: string) => this._participantController.connectWithParticipant(id) + ); + this.registerCommand( + EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, + (name: string) => + this._participantController.selectDatabaseWithParticipant(name) + ); + this.registerCommand( + EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, + (name: string) => + this._participantController.selectCollectionWithParticipant(name) + ); }; registerParticipantCommand = ( diff --git a/src/participant/participant.ts b/src/participant/participant.ts index ded24c582..d89ade7a5 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1,48 +1,71 @@ import * as vscode from 'vscode'; +import type { DataService } from 'mongodb-data-service'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; +import type { LoadedConnection } from '../storage/connectionStorage'; import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; import { GenericPrompt } from './prompts/generic'; import { CHAT_PARTICIPANT_ID } from './constants'; import { QueryPrompt } from './prompts/query'; +import { NamespacePrompt } from './prompts/namespace'; const log = createLogger('participant'); +enum QUERY_GENERATION_STATE { + DEFAULT = 'DEFAULT', + ASK_TO_CONNECT = 'ASK_TO_CONNECT', + ASK_FOR_DATABASE_NAME = 'ASK_FOR_DATABASE_NAME', + ASK_FOR_COLLECTION_NAME = 'ASK_FOR_COLLECTION_NAME', + READY_TO_GENERATE_QUERY = 'READY_TO_GENERATE_QUERY', +} + interface ChatResult extends vscode.ChatResult { metadata: { - command: string; - databaseName?: string; - collectionName?: string; - queryContent?: string; - description?: string; + responseContent?: string; }; - stream?: vscode.ChatResponseStream; } export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; -export function getRunnableContentFromString(responseContent: string) { - const matchedJSQueryContent = responseContent.match( - /```javascript((.|\n)*)```/ - ); - log.info('matchedJSQueryContent', matchedJSQueryContent); +const DB_NAME_ID = 'DATABASE_NAME'; +const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)\n`; + +const COL_NAME_ID = 'COLLECTION_NAME'; +const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)`; + +function parseForDatabaseAndCollectionName(text: string): { + databaseName?: string; + collectionName?: string; +} { + const databaseName = text.match(DB_NAME_REGEX)?.[1]; + const collectionName = text.match(COL_NAME_REGEX)?.[1]; + + return { databaseName, collectionName }; +} + +export function getRunnableContentFromString(text: string) { + const matchedJSresponseContent = text.match(/```javascript((.|\n)*)```/); + log.info('matchedJSresponseContent', matchedJSresponseContent); - const queryContent = - matchedJSQueryContent && matchedJSQueryContent.length > 1 - ? matchedJSQueryContent[1] + const responseContent = + matchedJSresponseContent && matchedJSresponseContent.length > 1 + ? matchedJSresponseContent[1] : ''; - log.info('queryContent', queryContent); - return queryContent; + log.info('responseContent', responseContent); + return responseContent; } export class ParticipantController { _participant?: vscode.ChatParticipant; - _chatResult: ChatResult; _connectionController: ConnectionController; _storageController: StorageController; + _queryGenerationState?: QUERY_GENERATION_STATE; + _chatResult?: ChatResult; + _databaseName?: string; + _collectionName?: string; constructor({ connectionController, @@ -51,7 +74,6 @@ export class ParticipantController { connectionController: ConnectionController; storageController: StorageController; }) { - this._chatResult = { metadata: { command: '' } }; this._connectionController = connectionController; this._storageController = storageController; } @@ -79,16 +101,27 @@ export class ParticipantController { return this._participant || this.createParticipant(context); } - handleEmptyQueryRequest(): ChatResult { - return { - metadata: { - command: '', - }, - errorDetails: { - message: - 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".', - }, - }; + handleEmptyQueryRequest(stream: vscode.ChatResponseStream): undefined { + let message; + switch (this._queryGenerationState) { + case QUERY_GENERATION_STATE.ASK_TO_CONNECT: + message = + 'Please select a cluster to connect by clicking on an item in the connections list.'; + break; + case QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME: + message = + 'Please select a database by either clicking on an item in the list or typing the name manually in the chat.'; + break; + case QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME: + message = + 'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.'; + break; + default: + message = + 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".'; + } + stream.markdown(vscode.l10n.t(`${message}\n\n`)); + return; } handleError(err: any, stream: vscode.ChatResponseStream): void { @@ -133,9 +166,7 @@ export class ParticipantController { const chatResponse = await model.sendRequest(messages, {}, token); for await (const fragment of chatResponse.text) { responseContent += fragment; - stream.markdown(fragment); } - stream.markdown('\n\n'); } } catch (err) { this.handleError(err, stream); @@ -145,17 +176,12 @@ export class ParticipantController { } // @MongoDB what is mongodb? - async handleGenericRequest({ - request, - context, - stream, - token, - }: { - request: vscode.ChatRequest; - context: vscode.ChatContext; - stream: vscode.ChatResponseStream; - token: vscode.CancellationToken; - }) { + async handleGenericRequest( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ): Promise { const messages = GenericPrompt.buildMessages({ request, context, @@ -171,10 +197,10 @@ export class ParticipantController { stream, token, }); + stream.markdown(responseContent); - const queryContent = getRunnableContentFromString(responseContent); - - if (queryContent && queryContent.trim().length) { + const runnableContent = getRunnableContentFromString(responseContent); + if (runnableContent && runnableContent.trim().length) { stream.button({ command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, title: vscode.l10n.t('▶️ Run'), @@ -185,58 +211,305 @@ export class ParticipantController { title: vscode.l10n.t('Open in playground'), }); - return { - metadata: { - command: '', - stream, - queryContent, - }, - }; + return { metadata: { responseContent: runnableContent } }; } - return { metadata: { command: '' } }; + return { metadata: {} }; } - // @MongoDB /query find all documents where the "address" has the word Broadway in it. - async handleQueryRequest({ - request, - context, - stream, - token, + async connectWithParticipant(id: string): Promise { + if (!id) { + await this._connectionController.connectWithURI(); + } else { + await this._connectionController.connectWithConnectionId(id); + } + + const connectionName = this._connectionController.getActiveConnectionName(); + if (connectionName) { + this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; + } + + return vscode.commands.executeCommand('workbench.action.chat.open', { + query: `@MongoDB /query ${connectionName}`, + }); + } + + _createMarkdownLink({ + commandId, + query, + name, }: { - request: vscode.ChatRequest; - context: vscode.ChatContext; - stream: vscode.ChatResponseStream; - token: vscode.CancellationToken; - }) { - if (!request.prompt || request.prompt.trim().length === 0) { - return this.handleEmptyQueryRequest(); + commandId: string; + query?: string; + name: string; + }): vscode.MarkdownString { + const commandQuery = query ? `?%5B%22${query}%22%5D` : ''; + const connName = new vscode.MarkdownString( + `- ${name}\n` + ); + connName.supportHtml = true; + connName.isTrusted = { enabledCommands: [commandId] }; + return connName; + } + + // TODO (VSCODE-589): Evaluate the usability of displaying all existing connections in the list. + // Consider introducing a "recent connections" feature to display only a limited number of recent connections, + // with a "Show more" link that opens the Command Palette for access to the full list. + // If we implement this, the "Add new connection" link may become redundant, + // as this option is already available in the Command Palette dropdown. + getConnectionsTree(): vscode.MarkdownString[] { + return [ + this._createMarkdownLink({ + commandId: 'mdb.connectWithParticipant', + name: 'Add new connection', + }), + ...Object.values(this._connectionController._connections) + .sort((connectionA: LoadedConnection, connectionB: LoadedConnection) => + (connectionA.name || '').localeCompare(connectionB.name || '') + ) + .map((conn: LoadedConnection) => + this._createMarkdownLink({ + commandId: 'mdb.connectWithParticipant', + query: conn.id, + name: conn.name, + }) + ), + ]; + } + + async selectDatabaseWithParticipant(name: string): Promise { + this._databaseName = name; + return vscode.commands.executeCommand('workbench.action.chat.open', { + query: `@MongoDB /query ${name}`, + }); + } + + async selectCollectionWithParticipant(name: string): Promise { + this._collectionName = name; + return vscode.commands.executeCommand('workbench.action.chat.open', { + query: `@MongoDB /query ${name}`, + }); + } + + // TODO (VSCODE-589): Display only 10 items in clickable lists with the show more option. + async getDatabasesTree( + dataService: DataService + ): Promise { + try { + const databases = await dataService.listDatabases({ + nameOnly: true, + }); + return databases.map((db) => + this._createMarkdownLink({ + commandId: 'mdb.selectDatabaseWithParticipant', + query: db.name, + name: db.name, + }) + ); + } catch (error) { + // Users can always do this manually when asked to provide a database name. + return []; + } + } + + // TODO (VSCODE-589): Display only 10 items in clickable lists with the show more option. + async getCollectionTree( + dataService: DataService + ): Promise { + if (!this._databaseName) { + return []; + } + + try { + const collections = await dataService.listCollections(this._databaseName); + return collections.map((coll) => + this._createMarkdownLink({ + commandId: 'mdb.selectCollectionWithParticipant', + query: coll.name, + name: coll.name, + }) + ); + } catch (error) { + // Users can always do this manually when asked to provide a collection name. + return []; + } + } + + _ifNewChatResetQueryGenerationState(context: vscode.ChatContext): void { + const isNewChat = !context.history.find( + (historyItem) => historyItem.participant === CHAT_PARTICIPANT_ID + ); + + if (isNewChat) { + this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; + this._chatResult = undefined; + this._databaseName = undefined; + this._collectionName = undefined; + } + } + + _waitingForUserToProvideNamespace(prompt: string): boolean { + if ( + !this._queryGenerationState || + ![ + QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME, + QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME, + ].includes(this._queryGenerationState) + ) { + return false; + } + + if ( + this._queryGenerationState === + QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME + ) { + this._databaseName = prompt; + if (!this._collectionName) { + this._queryGenerationState = + QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME; + return true; + } + return false; + } + + if ( + this._queryGenerationState === + QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME + ) { + this._collectionName = prompt; + if (!this._databaseName) { + this._queryGenerationState = + QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME; + return true; + } + this._queryGenerationState = + QUERY_GENERATION_STATE.READY_TO_GENERATE_QUERY; + return false; + } + + return false; + } + + async _shouldAskForNamespace( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ): Promise { + if (this._waitingForUserToProvideNamespace(request.prompt)) { + return true; } - let dataService = this._connectionController.getActiveDataService(); + if (this._databaseName && this._collectionName) { + return false; + } + + const messagesWithNamespace = NamespacePrompt.buildMessages({ + context, + request, + }); + const responseContentWithNamespace = await this.getChatResponseContent({ + messages: messagesWithNamespace, + stream, + token, + }); + const namespace = parseForDatabaseAndCollectionName( + responseContentWithNamespace + ); + + this._databaseName = namespace.databaseName || this._databaseName; + this._collectionName = namespace.collectionName || this._collectionName; + + if (namespace.databaseName && namespace.collectionName) { + this._queryGenerationState = + QUERY_GENERATION_STATE.READY_TO_GENERATE_QUERY; + return false; + } + + return true; + } + + async _askForNamespace( + request: vscode.ChatRequest, + stream: vscode.ChatResponseStream + ): Promise { + const dataService = this._connectionController.getActiveDataService(); if (!dataService) { + this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; + return; + } + + // If no database or collection name is found in the user prompt, + // we retrieve the available namespaces from the current connection. + // Users can then select a value by clicking on an item in the list. + if (!this._databaseName) { + const tree = await this.getDatabasesTree(dataService); stream.markdown( - "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against.\n\n" + 'What is the name of the database you would like this query to run against?\n\n' ); - // We add a delay so the user can read the message. - // TODO: maybe there is better way to handle this. - // stream.button() does not awaits so we can't use it here. - // Followups do not support input so we can't use that either. - await new Promise((resolve) => setTimeout(resolve, 1000)); - const successfullyConnected = - await this._connectionController.changeActiveConnection(); - dataService = this._connectionController.getActiveDataService(); - - if (!dataService || !successfullyConnected) { - stream.markdown( - 'No connection for command provided. Please use a valid connection for running commands.\n\n' - ); - return { metadata: { command: '' } }; + for (const item of tree) { + stream.markdown(item); } - + this._queryGenerationState = QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME; + } else if (!this._collectionName) { + const tree = await this.getCollectionTree(dataService); stream.markdown( - `Connected to "${this._connectionController.getActiveConnectionName()}".\n\n` + 'Which collection would you like to query within this database?\n\n' ); + for (const item of tree) { + stream.markdown(item); + } + this._queryGenerationState = + QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME; + } + + return; + } + + _shouldAskToConnectIfNotConnected( + stream: vscode.ChatResponseStream + ): boolean { + const dataService = this._connectionController.getActiveDataService(); + if (dataService) { + return false; + } + + const tree = this.getConnectionsTree(); + stream.markdown( + "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against.\n\n" + ); + for (const item of tree) { + stream.markdown(item); + } + this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; + return true; + } + + // @MongoDB /query find all documents where the "address" has the word Broadway in it. + async handleQueryRequest( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ): Promise { + // TODO: Reset this._queryGenerationState to QUERY_GENERATION_STATE.DEFAULT + // when a command other than /query is called, as it disrupts the flow. + this._ifNewChatResetQueryGenerationState(context); + + if (this._shouldAskToConnectIfNotConnected(stream)) { + return { metadata: {} }; + } + + const shouldAskForNamespace = await this._shouldAskForNamespace( + request, + context, + stream, + token + ); + + if (shouldAskForNamespace) { + await this._askForNamespace(request, stream); + return { metadata: {} }; } const abortController = new AbortController(); @@ -245,55 +518,53 @@ export class ParticipantController { }); const messages = QueryPrompt.buildMessages({ - context, request, + context, + databaseName: this._databaseName, + collectionName: this._collectionName, }); - const responseContent = await this.getChatResponseContent({ messages, stream, token, }); - const queryContent = getRunnableContentFromString(responseContent); - if (!queryContent || queryContent.trim().length === 0) { - return { metadata: { command: '' } }; - } + stream.markdown(responseContent); + this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; - stream.button({ - command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, - title: vscode.l10n.t('▶️ Run'), - }); - stream.button({ - command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, - title: vscode.l10n.t('Open in playground'), - }); + const runnableContent = getRunnableContentFromString(responseContent); + if (runnableContent && runnableContent.trim().length) { + stream.button({ + command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, + title: vscode.l10n.t('▶️ Run'), + }); + stream.button({ + command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + title: vscode.l10n.t('Open in playground'), + }); + } - return { - metadata: { - command: '', - stream, - queryContent, - }, - }; + return { metadata: { responseContent: runnableContent } }; } async chatHandler( - request: vscode.ChatRequest, - context: vscode.ChatContext, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken - ): Promise { + ...args: [ + vscode.ChatRequest, + vscode.ChatContext, + vscode.ChatResponseStream, + vscode.CancellationToken + ] + ): Promise { + const [request, , stream] = args; const hasBeenShownWelcomeMessageAlready = !!this._storageController.get( StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE ); - if (!hasBeenShownWelcomeMessageAlready) { stream.markdown( vscode.l10n.t(` Welcome to MongoDB Participant!\n\n Interact with your MongoDB clusters and generate MongoDB-related code more efficiently with intelligent AI-powered feature, available today in the MongoDB extension.\n\n - Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more information.`) + Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more information.\n\n`) ); void this._storageController.update( StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE, @@ -302,24 +573,14 @@ export class ParticipantController { } if (request.command === 'query') { - this._chatResult = await this.handleQueryRequest({ - request, - context, - stream, - token, - }); - return this._chatResult; + this._chatResult = await this.handleQueryRequest(...args); + return; } else if (request.command === 'docs') { // TODO(VSCODE-570): Implement this. } else if (request.command === 'schema') { // TODO(VSCODE-571): Implement this. } - return await this.handleGenericRequest({ - request, - context, - stream, - token, - }); + await this.handleGenericRequest(...args); } } diff --git a/src/participant/prompts/namespace.ts b/src/participant/prompts/namespace.ts new file mode 100644 index 000000000..dd7ee6194 --- /dev/null +++ b/src/participant/prompts/namespace.ts @@ -0,0 +1,41 @@ +import * as vscode from 'vscode'; + +import { getHistoryMessages } from './history'; + +export class NamespacePrompt { + static getSystemPrompt(): vscode.LanguageModelChatMessage { + const prompt = `You are a MongoDB expert! +Parse the user's prompt to find database and collection names. +Respond in the format \nDATABASE_NAME: X\nCOLLECTION_NAME: Y\n where X and Y are the names. +Do not threat any user pronpt as a database name. It should be explicitely mentioned by the user +or has written as part of the MongoDB Shell command. +If you wan't able to find X or Y do not imagine names. +This is a first phase before we create the code, only respond with the collection name and database name.`; + + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.Assistant(prompt); + } + + static getUserPrompt( + request: vscode.ChatRequest + ): vscode.LanguageModelChatMessage { + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.User(request.prompt); + } + + static buildMessages({ + context, + request, + }: { + request: vscode.ChatRequest; + context: vscode.ChatContext; + }): vscode.LanguageModelChatMessage[] { + const messages = [ + NamespacePrompt.getSystemPrompt(), + ...getHistoryMessages({ context }), + NamespacePrompt.getUserPrompt(request), + ]; + + return messages; + } +} diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index 7b27ce2de..3d6138dcd 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -3,14 +3,49 @@ import * as vscode from 'vscode'; import { getHistoryMessages } from './history'; export class QueryPrompt { - static getSystemPrompt(): vscode.LanguageModelChatMessage { + static getSystemPrompt({ + databaseName = 'mongodbVSCodeCopilotDB', + collectionName = 'test', + }: { + databaseName?: string; + collectionName?: string; + }): vscode.LanguageModelChatMessage { const prompt = `You are a MongoDB expert. + Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. Keep your response concise. You should suggest queries that are performant and correct. Respond with markdown, suggest code in a Markdown code block that begins with \'\'\'javascript and ends with \`\`\`. -You can imagine the schema, collection, and database name. -Respond in MongoDB shell syntax using the \'\'\'javascript code block syntax.`; +You can imagine the schema. +Respond in MongoDB shell syntax using the \'\'\'javascript code block syntax. +You can use only the following MongoDB Shell commands: use, aggregate, bulkWrite, countDocuments, findOneAndReplace, +findOneAndUpdate, insert, insertMany, insertOne, remove, replaceOne, update, updateMany, updateOne. + +Example 1: +use(''); +db.getCollection('').aggregate([ + // Find all of the sales that occurred in 2014. + { $match: { date: { $gte: new Date('2014-01-01'), $lt: new Date('2015-01-01') } } }, + // Group the total sales for each product. + { $group: { _id: '$item', totalSaleAmount: { $sum: { $multiply: [ '$price', '$quantity' ] } } } } +]); + +Example 2: +use(''); +db.getCollection('').find({ + date: { $gte: new Date('2014-04-04'), $lt: new Date('2014-04-05') } +}).count(); + +Database name: ${databaseName} +Collection name: ${collectionName} + +MongoDB command to specify database: +use(''); + +MongoDB command to specify collection: +db.getCollection('') + +Explain the code snippet you have generated.`; // eslint-disable-next-line new-cap return vscode.LanguageModelChatMessage.Assistant(prompt); @@ -26,12 +61,16 @@ Respond in MongoDB shell syntax using the \'\'\'javascript code block syntax.`; static buildMessages({ context, request, + databaseName, + collectionName, }: { request: vscode.ChatRequest; context: vscode.ChatContext; + databaseName?: string; + collectionName?: string; }): vscode.LanguageModelChatMessage[] { const messages = [ - QueryPrompt.getSystemPrompt(), + QueryPrompt.getSystemPrompt({ databaseName, collectionName }), ...getHistoryMessages({ context }), QueryPrompt.getUserPrompt(request), ]; From beb23f9683c4e15c5797a455bb5a967ca6414f11 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 20 Aug 2024 11:00:11 +0200 Subject: [PATCH 07/45] test: setup participant unit tests VSCODE-592 (#781) * test: setup participant unit tests VSCODE-592 * refactor: keep one restore * refactor: remove sandbox --- src/editors/playgroundController.ts | 2 +- src/mdbExtensionController.ts | 2 +- src/participant/participant.ts | 22 +- .../editors/playgroundController.test.ts | 2 +- ...aygroundSelectedCodeActionProvider.test.ts | 2 +- .../language/languageServerController.test.ts | 2 +- .../suite/participant/participant.test.ts | 284 ++++++++++++++++++ 7 files changed, 297 insertions(+), 19 deletions(-) create mode 100644 src/test/suite/participant/participant.test.ts diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 187f320fa..211682485 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -45,7 +45,7 @@ import { isPlayground, getPlaygroundExtensionForTelemetry, } from '../utils/playground'; -import type { ParticipantController } from '../participant/participant'; +import type ParticipantController from '../participant/participant'; const log = createLogger('playground controller'); diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 008d46002..c6fee71d1 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -40,7 +40,7 @@ import WebviewController from './views/webviewController'; import { createIdFactory, generateId } from './utils/objectIdHelper'; import { ConnectionStorage } from './storage/connectionStorage'; import type StreamProcessorTreeItem from './explorer/streamProcessorTreeItem'; -import { ParticipantController } from './participant/participant'; +import ParticipantController from './participant/participant'; // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. diff --git a/src/participant/participant.ts b/src/participant/participant.ts index d89ade7a5..631a759ce 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -14,7 +14,7 @@ import { NamespacePrompt } from './prompts/namespace'; const log = createLogger('participant'); -enum QUERY_GENERATION_STATE { +export enum QUERY_GENERATION_STATE { DEFAULT = 'DEFAULT', ASK_TO_CONNECT = 'ASK_TO_CONNECT', ASK_FOR_DATABASE_NAME = 'ASK_FOR_DATABASE_NAME', @@ -36,29 +36,25 @@ const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)\n`; const COL_NAME_ID = 'COLLECTION_NAME'; const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)`; -function parseForDatabaseAndCollectionName(text: string): { +export function parseForDatabaseAndCollectionName(text: string): { databaseName?: string; collectionName?: string; } { - const databaseName = text.match(DB_NAME_REGEX)?.[1]; - const collectionName = text.match(COL_NAME_REGEX)?.[1]; - + const databaseName = text.match(DB_NAME_REGEX)?.[1].trim(); + const collectionName = text.match(COL_NAME_REGEX)?.[1].trim(); return { databaseName, collectionName }; } export function getRunnableContentFromString(text: string) { const matchedJSresponseContent = text.match(/```javascript((.|\n)*)```/); - log.info('matchedJSresponseContent', matchedJSresponseContent); - - const responseContent = + const code = matchedJSresponseContent && matchedJSresponseContent.length > 1 ? matchedJSresponseContent[1] : ''; - log.info('responseContent', responseContent); - return responseContent; + return code; } -export class ParticipantController { +export default class ParticipantController { _participant?: vscode.ChatParticipant; _connectionController: ConnectionController; _storageController: StorageController; @@ -191,7 +187,6 @@ export class ParticipantController { token.onCancellationRequested(() => { abortController.abort(); }); - const responseContent = await this.getChatResponseContent({ messages, stream, @@ -339,7 +334,6 @@ export class ParticipantController { const isNewChat = !context.history.find( (historyItem) => historyItem.participant === CHAT_PARTICIPANT_ID ); - if (isNewChat) { this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; this._chatResult = undefined; @@ -506,7 +500,6 @@ export class ParticipantController { stream, token ); - if (shouldAskForNamespace) { await this._askForNamespace(request, stream); return { metadata: {} }; @@ -523,6 +516,7 @@ export class ParticipantController { databaseName: this._databaseName, collectionName: this._collectionName, }); + const responseContent = await this.getChatResponseContent({ messages, stream, diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index 0526aeb34..c29343c69 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -22,7 +22,7 @@ import { StorageController } from '../../../storage'; import TelemetryService from '../../../telemetry/telemetryService'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { ExtensionContextStub, LanguageServerControllerStub } from '../stubs'; -import { ParticipantController } from '../../../participant/participant'; +import ParticipantController from '../../../participant/participant'; const expect = chai.expect; diff --git a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts index 07f9687d6..d1ac86c1e 100644 --- a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts +++ b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts @@ -13,7 +13,7 @@ import type { PlaygroundResult } from '../../../types/playgroundType'; import { ExportToLanguageMode } from '../../../types/playgroundType'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { ExtensionContextStub } from '../stubs'; -import { ParticipantController } from '../../../participant/participant'; +import ParticipantController from '../../../participant/participant'; import ConnectionController from '../../../connectionController'; import StatusView from '../../../views/statusView'; import StorageController from '../../../storage/storageController'; diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index 3143f9c1b..b86f2f975 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -22,7 +22,7 @@ import { StorageController } from '../../../storage'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import TelemetryService from '../../../telemetry/telemetryService'; import { ExtensionContextStub } from '../stubs'; -import { ParticipantController } from '../../../participant/participant'; +import ParticipantController from '../../../participant/participant'; const expect = chai.expect; diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts new file mode 100644 index 000000000..9f5c8f986 --- /dev/null +++ b/src/test/suite/participant/participant.test.ts @@ -0,0 +1,284 @@ +import * as vscode from 'vscode'; +import { beforeEach, afterEach } from 'mocha'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import type { DataService } from 'mongodb-data-service'; + +import ParticipantController, { + parseForDatabaseAndCollectionName, + getRunnableContentFromString, + QUERY_GENERATION_STATE, +} from '../../../participant/participant'; +import ConnectionController from '../../../connectionController'; +import { StorageController } from '../../../storage'; +import { StatusView } from '../../../views'; +import { ExtensionContextStub } from '../stubs'; +import TelemetryService from '../../../telemetry/telemetryService'; +import { TEST_DATABASE_URI } from '../dbTestHelper'; +import { CHAT_PARTICIPANT_ID } from '../../../participant/constants'; + +suite('Participant Controller Test Suite', function () { + const extensionContextStub = new ExtensionContextStub(); + + // The test extension runner. + extensionContextStub.extensionPath = '../../'; + + let testConnectionController: ConnectionController; + let testStorageController: StorageController; + let testStatusView: StatusView; + let testTelemetryService: TelemetryService; + let testParticipantController: ParticipantController; + let chatContextStub; + let chatStreamStub; + let chatTokenStub; + + beforeEach(function () { + testStorageController = new StorageController(extensionContextStub); + testStatusView = new StatusView(extensionContextStub); + testTelemetryService = new TelemetryService( + testStorageController, + extensionContextStub + ); + testConnectionController = new ConnectionController({ + statusView: testStatusView, + storageController: testStorageController, + telemetryService: testTelemetryService, + }); + testParticipantController = new ParticipantController({ + connectionController: testConnectionController, + storageController: testStorageController, + }); + sinon.replace( + testParticipantController._connectionController, + 'getActiveDataService', + () => + ({ + listDatabases: () => Promise.resolve([{ name: 'dbOne' }]), + listCollections: () => Promise.resolve([{ name: 'collOne' }]), + getMongoClientConnectionOptions: () => ({ + url: TEST_DATABASE_URI, + options: {}, + }), + once: sinon.stub(), + } as unknown as DataService) + ); + chatContextStub = { + history: [ + { + participant: CHAT_PARTICIPANT_ID, + prompt: 'hi', + response: 'hello', + }, + ], + }; + chatStreamStub = { + markdown: sinon.fake(), + button: sinon.fake(), + }; + chatTokenStub = { + onCancellationRequested: () => {}, + }; + // The model returned by vscode.lm.selectChatModels is always undefined in tests. + sinon.replace( + vscode.lm, + 'selectChatModels', + sinon.fake.returns([ + { + id: 'modelId', + vendor: 'copilot', + family: 'gpt-4o', + version: 'gpt-4o-date', + name: 'GPT 4o (date)', + maxInputTokens: 16211, + countTokens: () => {}, + sendRequest: () => + Promise.resolve({ + text: [ + '```javascript\n' + + "use('dbOne');\n" + + "db.getCollection('collOne').find({ name: 'example' });\n" + + '```', + ], + }), + }, + ]) + ); + }); + + afterEach(function () { + sinon.restore(); + }); + + test('parses a returned by ai text for database and collection name', function () { + const text = 'DATABASE_NAME: my \nCOLLECTION_NAME: cats'; + const { databaseName, collectionName } = + parseForDatabaseAndCollectionName(text); + expect(databaseName).to.be.equal('my'); + expect(collectionName).to.be.equal('cats'); + }); + + test('parses a returned by ai text for code blocks', function () { + const text = + '```javascript\n' + + "use('test');\n" + + "db.getCollection('test').find({ name: 'Shika' });\n" + + '```'; + const code = getRunnableContentFromString(text); + expect(code).to.be.equal( + "\nuse('test');\ndb.getCollection('test').find({ name: 'Shika' });\n" + ); + }); + + suite('when connected', function () { + beforeEach(function () { + sinon + .stub(testParticipantController, '_shouldAskToConnectIfNotConnected') + .returns(false); + }); + + suite('when has not been shown a welcome message yet', function () { + beforeEach(function () { + sinon.replace( + testParticipantController._storageController, + 'get', + sinon.fake.returns(false) + ); + }); + + test('prints the message to chat', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const welcomeMessage = chatStreamStub.markdown.firstCall.args[0]; + expect(welcomeMessage).to.include('Welcome to MongoDB Participant!'); + }); + }); + + suite('when has been shown a welcome message already', function () { + beforeEach(function () { + sinon.replace( + testParticipantController._storageController, + 'get', + sinon.fake.returns(true) + ); + }); + + suite('known namespace', function () { + beforeEach(function () { + sinon.stub(testParticipantController, '_databaseName').value('dbOne'); + sinon + .stub(testParticipantController, '_collectionName') + .value('collOne'); + sinon + .stub(testParticipantController, '_shouldAskForNamespace') + .resolves(false); + }); + + test('generates a query', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + expect(testParticipantController._queryGenerationState).to.be.equal( + undefined + ); + expect(testParticipantController._chatResult).to.be.equal(undefined); + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.include( + "db.getCollection('collOne').find({ name: 'example' });" + ); + }); + }); + + suite('unknown namespace', function () { + test('asks for a namespace and generates a query', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + expect(testParticipantController._queryGenerationState).to.be.equal( + undefined + ); + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const askForDBMessage = chatStreamStub.markdown.getCall(0).args[0]; + expect(askForDBMessage).to.include( + 'What is the name of the database you would like this query to run against?' + ); + const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; + expect(listDBsMessage.value).to.include( + '- dbOne' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.be.eql(undefined); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME + ); + + chatRequestMock.prompt = 'dbOne'; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + + expect(testParticipantController._databaseName).to.be.equal('dbOne'); + const askForCollMessage = chatStreamStub.markdown.getCall(2).args[0]; + expect(askForCollMessage).to.include( + 'Which collection would you like to query within this database?' + ); + const listCollsMessage = chatStreamStub.markdown.getCall(3).args[0]; + expect(listCollsMessage.value).to.include( + '- collOne' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.be.eql(undefined); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME + ); + + chatRequestMock.prompt = 'collOne'; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + + expect(testParticipantController._collectionName).to.be.equal( + 'collOne' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.include( + "db.getCollection('collOne').find({ name: 'example' });" + ); + }); + }); + }); + }); +}); From 3b990e5b5c2a7be2e8ce8aa2f7e2a6aeee50450f Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 21 Aug 2024 11:43:08 +0200 Subject: [PATCH 08/45] test: cover participant with unit tests VSCODE-592 (#783) * test: cover participant with unit tests VSCODE-592 * refactor: access connections via getSavedConnections --- src/participant/participant.ts | 79 +++- .../suite/participant/participant.test.ts | 418 ++++++++++++++---- 2 files changed, 378 insertions(+), 119 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 631a759ce..11cd93c4f 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode'; -import type { DataService } from 'mongodb-data-service'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; @@ -97,27 +96,41 @@ export default class ParticipantController { return this._participant || this.createParticipant(context); } - handleEmptyQueryRequest(stream: vscode.ChatResponseStream): undefined { - let message; + async handleEmptyQueryRequest(): Promise<(string | vscode.MarkdownString)[]> { + const messages: (string | vscode.MarkdownString)[] = []; switch (this._queryGenerationState) { case QUERY_GENERATION_STATE.ASK_TO_CONNECT: - message = - 'Please select a cluster to connect by clicking on an item in the connections list.'; + messages.push( + vscode.l10n.t( + 'Please select a cluster to connect by clicking on an item in the connections list.' + ) + ); + messages.push(...this.getConnectionsTree()); break; case QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME: - message = - 'Please select a database by either clicking on an item in the list or typing the name manually in the chat.'; + messages.push( + vscode.l10n.t( + 'Please select a database by either clicking on an item in the list or typing the name manually in the chat.' + ) + ); + messages.push(...(await this.getDatabasesTree())); break; case QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME: - message = - 'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.'; + messages.push( + vscode.l10n.t( + 'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.' + ) + ); + messages.push(...(await this.getCollectionTree())); break; default: - message = - 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".'; + messages.push( + vscode.l10n.t( + 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".' + ) + ); } - stream.markdown(vscode.l10n.t(`${message}\n\n`)); - return; + return messages; } handleError(err: any, stream: vscode.ChatResponseStream): void { @@ -212,7 +225,7 @@ export default class ParticipantController { return { metadata: {} }; } - async connectWithParticipant(id: string): Promise { + async connectWithParticipant(id?: string): Promise { if (!id) { await this._connectionController.connectWithURI(); } else { @@ -258,7 +271,8 @@ export default class ParticipantController { commandId: 'mdb.connectWithParticipant', name: 'Add new connection', }), - ...Object.values(this._connectionController._connections) + ...this._connectionController + .getSavedConnections() .sort((connectionA: LoadedConnection, connectionB: LoadedConnection) => (connectionA.name || '').localeCompare(connectionB.name || '') ) @@ -287,9 +301,13 @@ export default class ParticipantController { } // TODO (VSCODE-589): Display only 10 items in clickable lists with the show more option. - async getDatabasesTree( - dataService: DataService - ): Promise { + async getDatabasesTree(): Promise { + const dataService = this._connectionController.getActiveDataService(); + if (!dataService) { + this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; + return []; + } + try { const databases = await dataService.listDatabases({ nameOnly: true, @@ -308,13 +326,17 @@ export default class ParticipantController { } // TODO (VSCODE-589): Display only 10 items in clickable lists with the show more option. - async getCollectionTree( - dataService: DataService - ): Promise { + async getCollectionTree(): Promise { if (!this._databaseName) { return []; } + const dataService = this._connectionController.getActiveDataService(); + if (!dataService) { + this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; + return []; + } + try { const collections = await dataService.listCollections(this._databaseName); return collections.map((coll) => @@ -437,7 +459,7 @@ export default class ParticipantController { // we retrieve the available namespaces from the current connection. // Users can then select a value by clicking on an item in the list. if (!this._databaseName) { - const tree = await this.getDatabasesTree(dataService); + const tree = await this.getDatabasesTree(); stream.markdown( 'What is the name of the database you would like this query to run against?\n\n' ); @@ -446,7 +468,7 @@ export default class ParticipantController { } this._queryGenerationState = QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME; } else if (!this._collectionName) { - const tree = await this.getCollectionTree(dataService); + const tree = await this.getCollectionTree(); stream.markdown( 'Which collection would you like to query within this database?\n\n' ); @@ -550,6 +572,15 @@ export default class ParticipantController { ] ): Promise { const [request, , stream] = args; + + if (!request.prompt || request.prompt.trim().length === 0) { + const messages = await this.handleEmptyQueryRequest(); + for (const msg of messages) { + stream.markdown(msg); + } + return; + } + const hasBeenShownWelcomeMessageAlready = !!this._storageController.get( StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE ); @@ -575,6 +606,6 @@ export default class ParticipantController { // TODO(VSCODE-571): Implement this. } - await this.handleGenericRequest(...args); + this._chatResult = await this.handleGenericRequest(...args); } } diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 9f5c8f986..cad2a596e 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -16,6 +16,10 @@ import { ExtensionContextStub } from '../stubs'; import TelemetryService from '../../../telemetry/telemetryService'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { CHAT_PARTICIPANT_ID } from '../../../participant/constants'; +import { + SecretStorageLocation, + StorageLocation, +} from '../../../storage/storageController'; suite('Participant Controller Test Suite', function () { const extensionContextStub = new ExtensionContextStub(); @@ -48,20 +52,6 @@ suite('Participant Controller Test Suite', function () { connectionController: testConnectionController, storageController: testStorageController, }); - sinon.replace( - testParticipantController._connectionController, - 'getActiveDataService', - () => - ({ - listDatabases: () => Promise.resolve([{ name: 'dbOne' }]), - listCollections: () => Promise.resolve([{ name: 'collOne' }]), - getMongoClientConnectionOptions: () => ({ - url: TEST_DATABASE_URI, - options: {}, - }), - once: sinon.stub(), - } as unknown as DataService) - ); chatContextStub = { history: [ { @@ -129,8 +119,149 @@ suite('Participant Controller Test Suite', function () { ); }); + suite('when not connected', function () { + let connectWithConnectionIdStub; + let connectWithURIStub; + + beforeEach(function () { + connectWithConnectionIdStub = sinon.stub( + testParticipantController._connectionController, + 'connectWithConnectionId' + ); + connectWithURIStub = sinon.stub( + testParticipantController._connectionController, + 'connectWithURI' + ); + sinon.replace( + testParticipantController._connectionController, + 'getActiveDataService', + () => null + ); + sinon.replace( + testParticipantController._storageController, + 'get', + sinon.fake.returns(true) + ); + sinon.replace( + testParticipantController._connectionController, + 'getSavedConnections', + () => [ + { + id: '123', + name: 'localhost', + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + connectionOptions: { connectionString: 'mongodb://localhost' }, + }, + ] + ); + }); + + test('asks to connect', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const connectMessage = chatStreamStub.markdown.getCall(0).args[0]; + expect(connectMessage).to.include( + "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against." + ); + const addNewConnectionMessage = + chatStreamStub.markdown.getCall(1).args[0]; + expect(addNewConnectionMessage.value).to.include( + '- Add new connection' + ); + const listConnectionsMessage = chatStreamStub.markdown.getCall(2).args[0]; + expect(listConnectionsMessage.value).to.include( + '- localhost' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.be.eql(undefined); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_TO_CONNECT + ); + }); + + test('handles empty connection name', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_TO_CONNECT + ); + + chatRequestMock.prompt = ''; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + + const emptyMessage = chatStreamStub.markdown.getCall(3).args[0]; + expect(emptyMessage).to.include( + 'Please select a cluster to connect by clicking on an item in the connections list.' + ); + const addNewConnectionMessage = + chatStreamStub.markdown.getCall(4).args[0]; + expect(addNewConnectionMessage.value).to.include( + '- Add new connection' + ); + const listConnectionsMessage = chatStreamStub.markdown.getCall(5).args[0]; + expect(listConnectionsMessage.value).to.include( + '- localhost' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.be.eql(undefined); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_TO_CONNECT + ); + }); + + test('calls connect by id for an existing connection', async function () { + await testParticipantController.connectWithParticipant('123'); + expect(connectWithConnectionIdStub).to.have.been.calledWithExactly('123'); + }); + + test('calls connect with uri for a new connection', async function () { + await testParticipantController.connectWithParticipant(); + expect(connectWithURIStub).to.have.been.called; + }); + }); + suite('when connected', function () { beforeEach(function () { + sinon.replace( + testParticipantController._connectionController, + 'getActiveDataService', + () => + ({ + listDatabases: () => Promise.resolve([{ name: 'dbOne' }]), + listCollections: () => Promise.resolve([{ name: 'collOne' }]), + getMongoClientConnectionOptions: () => ({ + url: TEST_DATABASE_URI, + options: {}, + }), + once: sinon.stub(), + } as unknown as DataService) + ); sinon .stub(testParticipantController, '_shouldAskToConnectIfNotConnected') .returns(false); @@ -145,7 +276,7 @@ suite('Participant Controller Test Suite', function () { ); }); - test('prints the message to chat', async function () { + test('prints a welcome message to chat', async function () { const chatRequestMock = { prompt: 'find all docs by a name example', command: 'query', @@ -171,21 +302,11 @@ suite('Participant Controller Test Suite', function () { ); }); - suite('known namespace', function () { - beforeEach(function () { - sinon.stub(testParticipantController, '_databaseName').value('dbOne'); - sinon - .stub(testParticipantController, '_collectionName') - .value('collOne'); - sinon - .stub(testParticipantController, '_shouldAskForNamespace') - .resolves(false); - }); - + suite('generic command', function () { test('generates a query', async function () { const chatRequestMock = { - prompt: 'find all docs by a name example', - command: 'query', + prompt: 'how to find documents in my collection?', + command: undefined, references: [], }; expect(testParticipantController._queryGenerationState).to.be.equal( @@ -206,77 +327,184 @@ suite('Participant Controller Test Suite', function () { }); }); - suite('unknown namespace', function () { - test('asks for a namespace and generates a query', async function () { - const chatRequestMock = { - prompt: 'find all docs by a name example', - command: 'query', - references: [], - }; - expect(testParticipantController._queryGenerationState).to.be.equal( - undefined - ); - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); - const askForDBMessage = chatStreamStub.markdown.getCall(0).args[0]; - expect(askForDBMessage).to.include( - 'What is the name of the database you would like this query to run against?' - ); - const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; - expect(listDBsMessage.value).to.include( - '- dbOne' - ); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.be.eql(undefined); - expect(testParticipantController._queryGenerationState).to.be.eql( - QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME - ); + suite('query command', function () { + suite('known namespace', function () { + beforeEach(function () { + sinon + .stub(testParticipantController, '_databaseName') + .value('dbOne'); + sinon + .stub(testParticipantController, '_collectionName') + .value('collOne'); + sinon + .stub(testParticipantController, '_shouldAskForNamespace') + .resolves(false); + }); - chatRequestMock.prompt = 'dbOne'; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + test('generates a query', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + expect(testParticipantController._queryGenerationState).to.be.equal( + undefined + ); + expect(testParticipantController._chatResult).to.be.equal( + undefined + ); + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.include( + "db.getCollection('collOne').find({ name: 'example' });" + ); + }); + }); - expect(testParticipantController._databaseName).to.be.equal('dbOne'); - const askForCollMessage = chatStreamStub.markdown.getCall(2).args[0]; - expect(askForCollMessage).to.include( - 'Which collection would you like to query within this database?' - ); - const listCollsMessage = chatStreamStub.markdown.getCall(3).args[0]; - expect(listCollsMessage.value).to.include( - '- collOne' - ); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.be.eql(undefined); - expect(testParticipantController._queryGenerationState).to.be.eql( - QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME - ); + suite('unknown namespace', function () { + test('asks for a namespace and generates a query', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + expect(testParticipantController._queryGenerationState).to.be.equal( + undefined + ); + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const askForDBMessage = chatStreamStub.markdown.getCall(0).args[0]; + expect(askForDBMessage).to.include( + 'What is the name of the database you would like this query to run against?' + ); + const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; + expect(listDBsMessage.value).to.include( + '- dbOne' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.be.eql(undefined); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME + ); - chatRequestMock.prompt = 'collOne'; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + chatRequestMock.prompt = 'dbOne'; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); - expect(testParticipantController._collectionName).to.be.equal( - 'collOne' - ); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.include( - "db.getCollection('collOne').find({ name: 'example' });" - ); + expect(testParticipantController._databaseName).to.be.equal( + 'dbOne' + ); + const askForCollMessage = + chatStreamStub.markdown.getCall(2).args[0]; + expect(askForCollMessage).to.include( + 'Which collection would you like to query within this database?' + ); + const listCollsMessage = chatStreamStub.markdown.getCall(3).args[0]; + expect(listCollsMessage.value).to.include( + '- collOne' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.be.eql(undefined); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME + ); + + chatRequestMock.prompt = 'collOne'; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + + expect(testParticipantController._collectionName).to.be.equal( + 'collOne' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.include( + "db.getCollection('collOne').find({ name: 'example' });" + ); + }); + + test('handles empty database name', async function () { + sinon + .stub(testParticipantController, '_queryGenerationState') + .value(QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME); + + const chatRequestMock = { + prompt: '', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + + const emptyMessage = chatStreamStub.markdown.getCall(0).args[0]; + expect(emptyMessage).to.include( + 'Please select a database by either clicking on an item in the list or typing the name manually in the chat.' + ); + const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; + expect(listDBsMessage.value).to.include( + '- dbOne' + ); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME + ); + }); + + test('handles empty collection name', async function () { + sinon + .stub(testParticipantController, '_queryGenerationState') + .value(QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME); + sinon + .stub(testParticipantController, '_databaseName') + .value('dbOne'); + + const chatRequestMock = { + prompt: '', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + + const emptyMessage = chatStreamStub.markdown.getCall(0).args[0]; + expect(emptyMessage).to.include( + 'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.' + ); + const listCollsMessage = chatStreamStub.markdown.getCall(1).args[0]; + expect(listCollsMessage.value).to.include( + '- collOne' + ); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME + ); + }); }); }); }); From acc745ff72880425e61b52dbd37719034df944a3 Mon Sep 17 00:00:00 2001 From: Rhys Date: Wed, 21 Aug 2024 12:02:52 -0400 Subject: [PATCH 09/45] chore(copilot-ai): add accuracy test file structure VSCODE-583 (#782) --- .gitignore | 1 + package-lock.json | 677 +++++++++++++++++- package.json | 4 + src/language/worker.ts | 28 +- src/participant/constants.ts | 1 + src/participant/participant.ts | 5 +- src/participant/prompts/generic.ts | 21 +- src/participant/prompts/namespace.ts | 49 +- src/participant/prompts/query.ts | 22 +- .../ai-accuracy-tests/ai-accuracy-tests.ts | 350 +++++++++ src/test/ai-accuracy-tests/ai-backend.ts | 103 +++ src/test/ai-accuracy-tests/assertions.ts | 85 +++ .../create-test-results-html-page.ts | 99 +++ .../fixtures/fixture-loader.ts | 99 +++ .../test-results-page-styles.css | 27 + src/test/ai-accuracy-tests/test-setup.ts | 30 + src/test/runTest.ts | 90 ++- src/test/suite/index.ts | 2 + 18 files changed, 1577 insertions(+), 116 deletions(-) create mode 100644 src/test/ai-accuracy-tests/ai-accuracy-tests.ts create mode 100644 src/test/ai-accuracy-tests/ai-backend.ts create mode 100644 src/test/ai-accuracy-tests/assertions.ts create mode 100644 src/test/ai-accuracy-tests/create-test-results-html-page.ts create mode 100644 src/test/ai-accuracy-tests/fixtures/fixture-loader.ts create mode 100644 src/test/ai-accuracy-tests/test-results-page-styles.css create mode 100644 src/test/ai-accuracy-tests/test-setup.ts diff --git a/.gitignore b/.gitignore index e41f34cfa..e5785ab14 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ constants.json .env .eslintcache .sbom +src/test/ai-accuracy-tests/test-results.html diff --git a/package-lock.json b/package-lock.json index f752b765c..d7bec7616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,13 +98,16 @@ "node-fetch": "^2.7.0", "node-loader": "^0.6.0", "npm-run-all": "^4.1.5", + "openai": "^4.55.7", "ora": "^5.4.1", "path-browserify": "^1.0.1", "pre-commit": "^1.2.2", "prettier": "^2.8.8", "process": "^0.11.10", + "rewiremock": "^3.14.5", "sinon": "^9.2.4", "sinon-chai": "^3.7.0", + "source-map-support": "^0.5.21", "stream-browserify": "^3.0.0", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.5.1", @@ -5652,6 +5655,17 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -6757,6 +6771,19 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -7000,6 +7027,34 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.4", + "util": "^0.10.4" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -7105,6 +7160,24 @@ "npm": ">=6" } }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -7425,6 +7498,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, "node_modules/browserslist": { "version": "4.23.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", @@ -7587,6 +7670,13 @@ "node": ">=10.0.0" } }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -7649,13 +7739,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8060,6 +8156,13 @@ "node": ">=14" } }, + "node_modules/compare-module-exports": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/compare-module-exports/-/compare-module-exports-2.1.0.tgz", + "integrity": "sha512-3Lc0sTIuX1jmY2K2RrXRJOND6KsRTX2D4v3+eu1PDptsuJZVK4LZc852eZa9I+avj0NrUKlTNgqvccNOH6mbGg==", + "dev": true, + "license": "ISC" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8122,6 +8225,19 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -8159,6 +8275,15 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -8814,16 +8939,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -9122,6 +9251,17 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -9548,6 +9688,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -11160,6 +11321,37 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -11328,15 +11520,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11738,11 +11935,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12037,6 +12235,13 @@ "node": ">=10.19.0" } }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", @@ -12059,6 +12264,16 @@ "node": ">=14.18.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/husky": { "version": "9.1.4", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.4.tgz", @@ -13429,6 +13644,13 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -13488,6 +13710,34 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -14728,6 +14978,125 @@ "node": ">= 6.13.0" } }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/node-libs-browser/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-libs-browser/node_modules/readable-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/node-libs-browser/node_modules/stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/node-libs-browser/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/node-loader": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-0.6.0.tgz", @@ -14986,9 +15355,13 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15152,6 +15525,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.55.7", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.55.7.tgz", + "integrity": "sha512-I2dpHTINt0Zk+Wlns6KzkKu77MmNW3VfIIQf5qYziEUI6t7WciG1zTobfKqdPzBmZi3TTM+3DtjPumxQdcvzwA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.44.tgz", + "integrity": "sha512-ZsbGerYg72WMXUIE9fYxtvfzLEuq6q8mKERdWFnqTmOvudMxnz+CBNRoOwJ2kNpFOncrKjT1hZwxjlFgQ9qvQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -15289,6 +15699,13 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/os-dns-native": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/os-dns-native/-/os-dns-native-1.2.1.tgz", @@ -16074,6 +16491,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -16673,6 +17099,23 @@ "node": ">=0.10.0" } }, + "node_modules/rewiremock": { + "version": "3.14.5", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.5.tgz", + "integrity": "sha512-MdPutvaUd+kKVz/lcEz6N6337s4PxRUR5vhphIp2/TJRgfXIckomIkCsIAbwB53MjiSLwi7KBMdQ9lPWE5WpYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-runtime": "^6.26.0", + "compare-module-exports": "^2.1.0", + "lodash.some": "^4.6.0", + "lodash.template": "^4.4.0", + "node-libs-browser": "^2.1.0", + "path-parse": "^1.0.5", + "wipe-node-cache": "^2.1.2", + "wipe-webpack-cache": "^2.1.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -17077,14 +17520,17 @@ } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -17168,13 +17614,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17493,6 +17944,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -17751,6 +18203,60 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-http/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-http/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-http/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -18360,6 +18866,19 @@ "node": ">=0.4" } }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tiny-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", @@ -18385,6 +18904,13 @@ "node": ">=14.14" } }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", @@ -18585,6 +19111,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==", + "dev": true, + "license": "MIT" + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -18822,6 +19355,20 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", @@ -18838,11 +19385,51 @@ "requires-port": "^1.0.0" } }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/url/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -18883,6 +19470,13 @@ "node": ">= 0.8" } }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-jsonrpc": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", @@ -19337,6 +19931,23 @@ "node": ">=0.1.90" } }, + "node_modules/wipe-node-cache": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-2.1.2.tgz", + "integrity": "sha512-m7NXa8qSxBGMtdQilOu53ctMaIBXy93FOP04EC1Uf4bpsE+r+adfLKwIMIvGbABsznaSNxK/ErD4xXDyY5og9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/wipe-webpack-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wipe-webpack-cache/-/wipe-webpack-cache-2.1.0.tgz", + "integrity": "sha512-OXzQMGpA7MnQQ8AG+uMl5mWR2ezy6fw1+DMHY+wzYP1qkF1jrek87psLBmhZEj+er4efO/GD4R8jXWFierobaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "wipe-node-cache": "^2.1.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index c385b4e1d..1723d0397 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "test": "npm run test-webview && npm run test-extension", "test-extension": "cross-env NODE_OPTIONS=--no-force-async-hooks-checks xvfb-maybe node ./out/test/runTest.js", "test-webview": "mocha -r ts-node/register --file ./src/test/setup-webview.ts src/test/suite/views/webview-app/**/*.test.tsx", + "ai-accuracy-tests": "mocha -r ts-node/register --file ./src/test/ai-accuracy-tests/test-setup.ts ./src/test/ai-accuracy-tests/ai-accuracy-tests.ts", "analyze-bundle": "webpack --mode production --analyze", "vscode:prepublish": "npm run clean && npm run compile:keyfile && npm run compile:resources && webpack --mode production", "check": "npm run lint && npm run depcheck", @@ -1238,13 +1239,16 @@ "node-fetch": "^2.7.0", "node-loader": "^0.6.0", "npm-run-all": "^4.1.5", + "openai": "^4.55.7", "ora": "^5.4.1", "path-browserify": "^1.0.1", "pre-commit": "^1.2.2", "prettier": "^2.8.8", "process": "^0.11.10", + "rewiremock": "^3.14.5", "sinon": "^9.2.4", "sinon-chai": "^3.7.0", + "source-map-support": "^0.5.21", "stream-browserify": "^3.0.0", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.5.1", diff --git a/src/language/worker.ts b/src/language/worker.ts index 316e9f3e5..3a1d1605d 100644 --- a/src/language/worker.ts +++ b/src/language/worker.ts @@ -26,7 +26,7 @@ const getContent = ({ type, printable }: EvaluationResult) => { : getEJSON(printable); }; -const getLanguage = (evaluationResult: EvaluationResult) => { +export const getLanguage = (evaluationResult: EvaluationResult) => { const content = getContent(evaluationResult); if (typeof content === 'object' && content !== null) { @@ -40,14 +40,27 @@ type ExecuteCodeOptions = { codeToEvaluate: string; connectionString: string; connectionOptions: MongoClientOptions; + onPrint?: (values: EvaluationResult[]) => void; filePath?: string; }; +function handleEvalPrint(values: EvaluationResult[]) { + parentPort?.postMessage({ + name: ServerCommands.SHOW_CONSOLE_OUTPUT, + payload: values.map((v) => { + return typeof v.printable === 'string' + ? v.printable + : util.inspect(v.printable); + }), + }); +} + /** * Execute code from a playground. */ -const execute = async ({ +export const execute = async ({ codeToEvaluate, + onPrint = handleEvalPrint, connectionString, connectionOptions, filePath, @@ -66,16 +79,7 @@ const execute = async ({ // Collect console.log() output. runtime.setEvaluationListener({ - onPrint(values: EvaluationResult[]) { - parentPort?.postMessage({ - name: ServerCommands.SHOW_CONSOLE_OUTPUT, - payload: values.map((v) => { - return typeof v.printable === 'string' - ? v.printable - : util.inspect(v.printable); - }), - }); - }, + onPrint, }); // In order to support local require directly from the file where code lives, we can not wrap the diff --git a/src/participant/constants.ts b/src/participant/constants.ts index e4aac61e0..d783a19d7 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -1 +1,2 @@ export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; +export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 11cd93c4f..5cd362a0e 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -7,7 +7,7 @@ import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; import { GenericPrompt } from './prompts/generic'; -import { CHAT_PARTICIPANT_ID } from './constants'; +import { CHAT_PARTICIPANT_ID, CHAT_PARTICIPANT_MODEL } from './constants'; import { QueryPrompt } from './prompts/query'; import { NamespacePrompt } from './prompts/namespace'; @@ -27,8 +27,6 @@ interface ChatResult extends vscode.ChatResult { }; } -export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; - const DB_NAME_ID = 'DATABASE_NAME'; const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)\n`; @@ -46,6 +44,7 @@ export function parseForDatabaseAndCollectionName(text: string): { export function getRunnableContentFromString(text: string) { const matchedJSresponseContent = text.match(/```javascript((.|\n)*)```/); + const code = matchedJSresponseContent && matchedJSresponseContent.length > 1 ? matchedJSresponseContent[1] diff --git a/src/participant/prompts/generic.ts b/src/participant/prompts/generic.ts index bde4604f0..3299e2426 100644 --- a/src/participant/prompts/generic.ts +++ b/src/participant/prompts/generic.ts @@ -3,37 +3,38 @@ import * as vscode from 'vscode'; import { getHistoryMessages } from './history'; export class GenericPrompt { - static getSystemPrompt(): vscode.LanguageModelChatMessage { + static getAssistantPrompt(): vscode.LanguageModelChatMessage { const prompt = `You are a MongoDB expert. Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. Keep your response concise. You should suggest queries that are performant and correct. -Respond with markdown, suggest code in a Markdown code block that begins with \'\'\'javascript and ends with \`\`\`. +Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. You can imagine the schema, collection, and database name. -Respond in MongoDB shell syntax using the \'\'\'javascript code block syntax.`; +Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; // eslint-disable-next-line new-cap return vscode.LanguageModelChatMessage.Assistant(prompt); } - static getUserPrompt( - request: vscode.ChatRequest - ): vscode.LanguageModelChatMessage { + static getUserPrompt(prompt: string): vscode.LanguageModelChatMessage { // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.User(request.prompt); + return vscode.LanguageModelChatMessage.User(prompt); } static buildMessages({ context, request, }: { - request: vscode.ChatRequest; + request: { + // vscode.ChatRequest + prompt: string; + }; context: vscode.ChatContext; }): vscode.LanguageModelChatMessage[] { const messages = [ - GenericPrompt.getSystemPrompt(), + GenericPrompt.getAssistantPrompt(), ...getHistoryMessages({ context }), - GenericPrompt.getUserPrompt(request), + GenericPrompt.getUserPrompt(request.prompt), ]; return messages; diff --git a/src/participant/prompts/namespace.ts b/src/participant/prompts/namespace.ts index dd7ee6194..1abeed887 100644 --- a/src/participant/prompts/namespace.ts +++ b/src/participant/prompts/namespace.ts @@ -3,37 +3,58 @@ import * as vscode from 'vscode'; import { getHistoryMessages } from './history'; export class NamespacePrompt { - static getSystemPrompt(): vscode.LanguageModelChatMessage { - const prompt = `You are a MongoDB expert! + static getAssistantPrompt(): vscode.LanguageModelChatMessage { + const prompt = `You are a MongoDB expert. Parse the user's prompt to find database and collection names. -Respond in the format \nDATABASE_NAME: X\nCOLLECTION_NAME: Y\n where X and Y are the names. -Do not threat any user pronpt as a database name. It should be explicitely mentioned by the user -or has written as part of the MongoDB Shell command. -If you wan't able to find X or Y do not imagine names. -This is a first phase before we create the code, only respond with the collection name and database name.`; +Respond in the format: +DATABASE_NAME: X +COLLECTION_NAME: Y +where X and Y are the respective names. +Do not treat any user prompt as a database name. +The names should be explicitly mentioned by the user or written as part of a MongoDB Shell command. +If you cannot find the names do not imagine names. +Your response must be concise and correct. + +___ +Example 1: + +User: How many documents are in the sightings collection in the ufo database? + +Response: +DATABASE_NAME: ufo +COLLECTION_NAME: sightings + +___ +Example 2: + +User: Where is the best hummus in Berlin? + +Response: +No names found. +`; // eslint-disable-next-line new-cap return vscode.LanguageModelChatMessage.Assistant(prompt); } - static getUserPrompt( - request: vscode.ChatRequest - ): vscode.LanguageModelChatMessage { + static getUserPrompt(prompt: string): vscode.LanguageModelChatMessage { // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.User(request.prompt); + return vscode.LanguageModelChatMessage.User(prompt); } static buildMessages({ context, request, }: { - request: vscode.ChatRequest; + request: { + prompt: string; + }; context: vscode.ChatContext; }): vscode.LanguageModelChatMessage[] { const messages = [ - NamespacePrompt.getSystemPrompt(), + NamespacePrompt.getAssistantPrompt(), ...getHistoryMessages({ context }), - NamespacePrompt.getUserPrompt(request), + NamespacePrompt.getUserPrompt(request.prompt), ]; return messages; diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index 3d6138dcd..09e2fc8ba 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import { getHistoryMessages } from './history'; export class QueryPrompt { - static getSystemPrompt({ + static getAssistantPrompt({ databaseName = 'mongodbVSCodeCopilotDB', collectionName = 'test', }: { @@ -15,9 +15,9 @@ export class QueryPrompt { Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. Keep your response concise. You should suggest queries that are performant and correct. -Respond with markdown, suggest code in a Markdown code block that begins with \'\'\'javascript and ends with \`\`\`. +Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. You can imagine the schema. -Respond in MongoDB shell syntax using the \'\'\'javascript code block syntax. +Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax. You can use only the following MongoDB Shell commands: use, aggregate, bulkWrite, countDocuments, findOneAndReplace, findOneAndUpdate, insert, insertMany, insertOne, remove, replaceOne, update, updateMany, updateOne. @@ -45,17 +45,15 @@ use(''); MongoDB command to specify collection: db.getCollection('') -Explain the code snippet you have generated.`; +Concisely explain the code snippet you have generated.`; // eslint-disable-next-line new-cap return vscode.LanguageModelChatMessage.Assistant(prompt); } - static getUserPrompt( - request: vscode.ChatRequest - ): vscode.LanguageModelChatMessage { + static getUserPrompt(prompt: string): vscode.LanguageModelChatMessage { // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.User(request.prompt); + return vscode.LanguageModelChatMessage.User(prompt); } static buildMessages({ @@ -64,15 +62,17 @@ Explain the code snippet you have generated.`; databaseName, collectionName, }: { - request: vscode.ChatRequest; + request: { + prompt: string; + }; context: vscode.ChatContext; databaseName?: string; collectionName?: string; }): vscode.LanguageModelChatMessage[] { const messages = [ - QueryPrompt.getSystemPrompt({ databaseName, collectionName }), + QueryPrompt.getAssistantPrompt({ databaseName, collectionName }), ...getHistoryMessages({ context }), - QueryPrompt.getUserPrompt(request), + QueryPrompt.getUserPrompt(request.prompt), ]; return messages; diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts new file mode 100644 index 000000000..fcc026d55 --- /dev/null +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -0,0 +1,350 @@ +/* eslint-disable no-console */ +import { expect } from 'chai'; +import { MongoClient } from 'mongodb'; +import { execFile as callbackExecFile } from 'child_process'; +import { MongoCluster } from 'mongodb-runner'; +import path from 'path'; +import util from 'util'; +import os from 'os'; +import * as vscode from 'vscode'; + +import { loadFixturesToDB } from './fixtures/fixture-loader'; +import type { Fixtures } from './fixtures/fixture-loader'; +import { AIBackend } from './ai-backend'; +import { GenericPrompt } from '../../participant/prompts/generic'; +import { QueryPrompt } from '../../participant/prompts/query'; +import { + createTestResultsHTMLPage, + type TestOutputs, + type TestResult, +} from './create-test-results-html-page'; +import { NamespacePrompt } from '../../participant/prompts/namespace'; +import { runCodeInMessage } from './assertions'; + +const numberOfRunsPerTest = 1; + +type AssertProps = { + responseContent: string; + connectionString: string; + fixtures: Fixtures; +}; + +type TestCase = { + testCase: string; + type: 'generic' | 'query' | 'namespace'; + userInput: string; + databaseName?: string; + collectionName?: string; + accuracyThresholdOverride?: number; + assertResult: (props: AssertProps) => Promise | void; + only?: boolean; // Translates to mocha's it.only so only this test will run. +}; + +const testCases: TestCase[] = [ + { + testCase: 'Basic query', + type: 'query', + databaseName: 'UFO', + collectionName: 'sightings', + userInput: 'How many documents are in the collection?', + assertResult: async ({ + responseContent, + connectionString, + }: AssertProps) => { + const result = await runCodeInMessage(responseContent, connectionString); + + const totalResponse = `${result.printOutput.join('')}${ + result.data?.result?.content + }`; + + const number = totalResponse.match(/\d+/); + expect(number?.[0]).to.equal('5'); + }, + }, +]; + +const projectRoot = path.join(__dirname, '..', '..', '..'); + +const execFile = util.promisify(callbackExecFile); + +const TEST_RESULTS_DB = 'test_generative_ai_accuracy_evergreen'; +const TEST_RESULTS_COL = 'evergreen_runs'; + +const DEFAULT_ATTEMPTS_PER_TEST = 2; +const ATTEMPTS_PER_TEST = process.env.AI_TESTS_ATTEMPTS_PER_TEST + ? +process.env.AI_TESTS_ATTEMPTS_PER_TEST + : DEFAULT_ATTEMPTS_PER_TEST; + +/** + * Insert the generative ai results to a db + * so we can track how they perform overtime. + */ +async function pushResultsToDB({ + results, + anyFailedAccuracyThreshold, + runTimeMS, + httpErrors, +}: { + results: TestResult[]; + anyFailedAccuracyThreshold: boolean; + runTimeMS: number; + httpErrors: number; +}) { + const client = new MongoClient( + process.env.AI_ACCURACY_RESULTS_MONGODB_CONNECTION_STRING || '' + ); + + try { + const database = client.db(TEST_RESULTS_DB); + const collection = database.collection(TEST_RESULTS_COL); + + const gitCommitHash = await execFile('git', ['rev-parse', 'HEAD'], { + cwd: projectRoot, + }); + + const doc = { + gitHash: gitCommitHash.stdout.trim(), + completedAt: new Date(), + attemptsPerTest: ATTEMPTS_PER_TEST, + anyFailedAccuracyThreshold, + httpErrors, + totalRunTimeMS: runTimeMS, // Total elapsed time including timeouts to avoid rate limit. + results: results.map((result) => { + const { 'Avg Execution Time (ms)': runTimeMS, Pass, ...rest } = result; + return { + runTimeMS, + Pass: Pass === '✓', + ...rest, + }; + }), + }; + + await collection.insertOne(doc); + } finally { + await client.close(); + } +} + +const buildMessages = (testCase: TestCase) => { + switch (testCase.type) { + case 'generic': + return GenericPrompt.buildMessages({ + request: { prompt: testCase.userInput }, + context: { history: [] }, + }); + + case 'query': + return QueryPrompt.buildMessages({ + request: { prompt: testCase.userInput }, + context: { history: [] }, + databaseName: testCase.databaseName, + collectionName: testCase.collectionName, + }); + + case 'namespace': + return NamespacePrompt.buildMessages({ + request: { prompt: testCase.userInput }, + context: { history: [] }, + }); + + default: + throw new Error(`Unknown test case type: ${testCase.type}`); + } +}; + +async function runTest({ + testCase, + aiBackend, +}: { + testCase: TestCase; + aiBackend: AIBackend; +}) { + const messages = buildMessages(testCase); + const chatCompletion = await aiBackend.runAIChatCompletionGeneration({ + messages: messages.map((message) => ({ + ...message, + role: + message.role === vscode.LanguageModelChatMessageRole.User + ? 'user' + : 'assistant', + })), + }); + + return chatCompletion; +} + +describe('AI Accuracy Tests', function () { + let cluster: MongoCluster; + let mongoClient: MongoClient; + let fixtures: Fixtures = {}; + let anyFailedAccuracyThreshold = false; + let startTime; + let aiBackend; + let connectionString: string; + + const results: TestResult[] = []; + const testOutputs: TestOutputs = {}; + + this.timeout(60_000 /* 1 min */); + + before(async function () { + console.log('Starting setup for AI accuracy tests...'); + + const startupStartTime = Date.now(); + + cluster = await MongoCluster.start({ + tmpDir: os.tmpdir(), + topology: 'standalone', + }); + console.log('Started a test cluster:', cluster.connectionString); + connectionString = cluster.connectionString; + + mongoClient = new MongoClient(cluster.connectionString); + + fixtures = await loadFixturesToDB({ + mongoClient, + }); + + aiBackend = new AIBackend('openai'); + + console.log(`Test setup complete in ${Date.now() - startupStartTime}ms.`); + console.log('Starting AI accuracy tests...'); + startTime = Date.now(); + }); + + after(async function () { + console.log('Finished AI accuracy tests.'); + console.log('Results:', results); + + console.table(results, [ + 'Type', + 'Test', + 'Namespace', + 'Accuracy', + 'Avg Execution Time (ms)', + 'Avg Prompt Tokens', + 'Avg Completion Tokens', + 'Pass', + ]); + + if (process.env.AI_ACCURACY_RESULTS_MONGODB_CONNECTION_STRING) { + await pushResultsToDB({ + results, + anyFailedAccuracyThreshold, + httpErrors: 0, // TODO + runTimeMS: Date.now() - startTime, + }); + } + + await mongoClient?.close(); + await cluster?.close(); + + const htmlPageLocation = await createTestResultsHTMLPage({ + testResults: results, + testOutputs, + }); + console.log('View prompts and responses here:'); + console.log(htmlPageLocation); + }); + + for (const testCase of testCases) { + const testFunction = testCase.only ? it.only : it; + + testFunction( + `should pass for input: "${testCase.userInput}" if average accuracy is above threshold`, + // eslint-disable-next-line no-loop-func + async function () { + console.log(`Starting test run of ${testCase.testCase}.`); + + const testRunDidSucceed: boolean[] = []; + const successFullRunStats: { + promptTokens: number; + completionTokens: number; + executionTimeMS: number; + }[] = []; + const accuracyThreshold = testCase.accuracyThresholdOverride ?? 0.8; + testOutputs[testCase.testCase] = { + prompt: testCase.userInput, + outputs: [], + }; + + for (let i = 0; i < numberOfRunsPerTest; i++) { + let success = false; + const startTime = Date.now(); + try { + const responseContent = await runTest({ + testCase, + aiBackend, + }); + testOutputs[testCase.testCase].outputs.push( + responseContent.content + ); + await testCase.assertResult({ + responseContent: responseContent.content, + connectionString, + fixtures, + }); + + successFullRunStats.push({ + completionTokens: responseContent.usageStats.completionTokens, + promptTokens: responseContent.usageStats.promptTokens, + executionTimeMS: Date.now() - startTime, + }); + success = true; + + console.log( + `Test run of ${testCase.testCase}. Run ${i} of ${numberOfRunsPerTest} succeeded` + ); + } catch (err) { + console.log( + `Test run of ${testCase.testCase}. Run ${i} of ${numberOfRunsPerTest} failed with error:`, + err + ); + } + + testRunDidSucceed.push(success); + } + + const averageAccuracy = + testRunDidSucceed.reduce((a, b) => a + (b ? 1 : 0), 0) / + testRunDidSucceed.length; + const didFail = averageAccuracy < accuracyThreshold; + + anyFailedAccuracyThreshold = anyFailedAccuracyThreshold || didFail; + + results.push({ + Test: testCase.testCase, + Type: testCase.type, + 'User Input': testCase.userInput.slice(0, 100), + Namespace: testCase.collectionName + ? `${testCase.databaseName}.${testCase.collectionName}` + : '', + Accuracy: averageAccuracy, + Pass: didFail ? '✗' : '✓', + 'Avg Execution Time (ms)': + successFullRunStats.length > 0 + ? successFullRunStats.reduce((a, b) => a + b.executionTimeMS, 0) / + successFullRunStats.length + : 0, + 'Avg Prompt Tokens': + successFullRunStats.length > 0 + ? successFullRunStats.reduce((a, b) => a + b.promptTokens, 0) / + successFullRunStats.length + : 0, + 'Avg Completion Tokens': + successFullRunStats.length > 0 + ? successFullRunStats.reduce( + (a, b) => a + b.completionTokens, + 0 + ) / successFullRunStats.length + : 0, + }); + + expect(averageAccuracy).to.be.at.least( + accuracyThreshold, + `Average accuracy (${averageAccuracy}) for input "${testCase.userInput}" is below the threshold (${accuracyThreshold})` + ); + } + ); + } +}); diff --git a/src/test/ai-accuracy-tests/ai-backend.ts b/src/test/ai-accuracy-tests/ai-backend.ts new file mode 100644 index 000000000..a9b746e1f --- /dev/null +++ b/src/test/ai-accuracy-tests/ai-backend.ts @@ -0,0 +1,103 @@ +import OpenAI from 'openai'; +import type { ChatCompletionCreateParamsBase } from 'openai/resources/chat/completions'; + +import { CHAT_PARTICIPANT_MODEL } from '../../participant/constants'; + +let openai: OpenAI; +function getOpenAIClient() { + if (!openai) { + openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }); + } + + return openai; +} + +export type AIService = 'openai'; + +type ChatMessage = { + role: 'user' | 'assistant'; + content: string; +}; +type ChatMessages = ChatMessage[]; + +type ChatCompletion = { + content: string; + usageStats: { + promptTokens: number; + completionTokens: number; + }; +}; + +async function createOpenAIChatCompletion({ + messages, + model = CHAT_PARTICIPANT_MODEL, +}: { + messages: ChatMessages; + model?: ChatCompletionCreateParamsBase['model']; +}): Promise { + const openai = getOpenAIClient(); + + // TODO: Currently we aren't supplying a system message, that may + // create a discrepancy in responses. We should investigate passing a system + // message, even if it's minimal. + const completion: OpenAI.Chat.Completions.ChatCompletion = + await openai.chat.completions.create({ + messages, + model, + }); + + return { + content: completion.choices[0].message.content || '', + usageStats: { + promptTokens: completion.usage?.prompt_tokens ?? NaN, + completionTokens: completion.usage?.completion_tokens ?? NaN, + }, + }; +} + +export type UsageStats = { promptTokens: number; completionTokens: number }; + +export type GenerationResponse = { + content: string; + query?: { + filter?: string; + project?: string; + sort?: string; + limit?: string; + skip?: string; + }; + aggregation?: string; + usageStats?: UsageStats; +}; + +export function createAIChatCompletion({ + messages, +}: { + messages: ChatMessages; + backend?: AIService; +}): Promise { + // Defaults to open ai for now + return createOpenAIChatCompletion({ messages }); +} + +export class AIBackend { + aiService: AIService; + + constructor(aiService: AIService) { + this.aiService = aiService; + } + + async runAIChatCompletionGeneration({ + messages, + }: { + messages: ChatMessages; + }): Promise { + const completion = await createAIChatCompletion({ + messages, + backend: this.aiService, + }); + return completion; + } +} diff --git a/src/test/ai-accuracy-tests/assertions.ts b/src/test/ai-accuracy-tests/assertions.ts new file mode 100644 index 000000000..988c749a3 --- /dev/null +++ b/src/test/ai-accuracy-tests/assertions.ts @@ -0,0 +1,85 @@ +import assert from 'assert'; +import util from 'util'; + +import type { Fixtures } from './fixtures/fixture-loader'; +import { getRunnableContentFromString } from '../../participant/participant'; +import { execute } from '../../language/worker'; +import type { ShellEvaluateResult } from '../../types/playgroundType'; + +export const runCodeInMessage = async ( + message: string, + connectionString: string +): Promise<{ + printOutput: string[]; + data: ShellEvaluateResult; + error: any; +}> => { + const codeToEvaluate = getRunnableContentFromString(message); + + if (codeToEvaluate.trim().length === 0) { + throw new Error(`no code found in message: ${message}`); + } + + const printOutput: string[] = []; + + const { data, error } = await execute({ + codeToEvaluate, + connectionString, + connectionOptions: { + productName: 'VSCode Copilot AI accuracy tests', + productDocsLink: 'N/A', + }, + onPrint: (values) => { + printOutput.push( + ...values.map((v) => + typeof v.printable === 'string' + ? v.printable + : util.inspect(v.printable) + ) + ); + }, + }); + + if (error) { + throw new Error( + `An error occurred when attempting to run the code in the message: \n${message}\n___Error:\n${error}` + ); + } + + return { + printOutput, + data, + error, + }; +}; + +export const isDeepStrictEqualTo = (expected: unknown) => (actual: unknown) => + assert.deepStrictEqual(actual, expected); + +export const isDeepStrictEqualToFixtures = + ( + db: string, + coll: string, + fixtures: Fixtures, + comparator: (document: Document) => boolean + ) => + (actual: unknown) => { + const expected = fixtures[db][coll].filter(comparator); + assert.deepStrictEqual(actual, expected); + }; + +export const anyOf = + (assertions: ((result: unknown) => void)[]) => (actual: unknown) => { + const errors: Error[] = []; + for (const assertion of assertions) { + try { + assertion(actual); + } catch (e) { + errors.push(e as Error); + } + } + + if (errors.length === assertions.length) { + throw errors[errors.length - 1]; + } + }; diff --git a/src/test/ai-accuracy-tests/create-test-results-html-page.ts b/src/test/ai-accuracy-tests/create-test-results-html-page.ts new file mode 100644 index 000000000..313f7f28b --- /dev/null +++ b/src/test/ai-accuracy-tests/create-test-results-html-page.ts @@ -0,0 +1,99 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +export type TestResult = { + Test: string; + Type: string; + 'User Input': string; + Namespace: string; + Accuracy: number; + Pass: '✗' | '✓'; + 'Avg Execution Time (ms)': number; + 'Avg Prompt Tokens': number; + 'Avg Completion Tokens': number; +}; + +type TestOutput = { + prompt: string; + outputs: string[]; +}; + +export type TestOutputs = { + [testName: string]: TestOutput; +}; + +function getTestResultsTable(testResults: TestResult[]) { + const headers = Object.keys(testResults[0]) + .map((key) => `${key}`) + .join(''); + + const resultRows = testResults + .map((result) => { + const row = Object.values(result) + .map((value) => `${value}`) + .join(''); + return `${row}`; + }) + .join(''); + + return ` + + + ${headers} + + + ${resultRows} + +
+`; +} + +function getTestOutputTables(testOutputs: TestOutputs) { + const outputTables = Object.entries(testOutputs) + .map(([testName, output]) => { + const outputRows = output.outputs + .map((out) => `${out}`) + .join(''); + return ` +

${testName}

+

Prompt: ${output.prompt}

+ + + + + + ${outputRows} + +
Outputs
+ `; + }) + .join(''); + + return outputTables; +} + +export async function createTestResultsHTMLPage({ + testResults, + testOutputs, +}: { + testResults: TestResult[]; + testOutputs: TestOutputs; +}): Promise { + const htmlOutput = ` + + Test Results + + + +

Test Results

+ ${getTestResultsTable(testResults)} +

Test Outputs

+ ${getTestOutputTables(testOutputs)} + +`; + + const htmlPageLocation = path.join(__dirname, 'test-results.html'); + await fs.writeFile(htmlPageLocation, htmlOutput); + + return htmlPageLocation; +} diff --git a/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts b/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts new file mode 100644 index 000000000..1423b1760 --- /dev/null +++ b/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts @@ -0,0 +1,99 @@ +import { promises as fs } from 'fs'; +import { EJSON } from 'bson'; +import type { Document } from 'bson'; +import path from 'path'; +import type { MongoClient } from 'mongodb'; + +export type Fixtures = { + [dbName: string]: { + [colName: string]: Document /* Extended JSON javascript object. */; + }; +}; + +function getDynamicDateFixture(): { + db: string; + coll: string; + documents: Document[]; +} { + return { + db: 'UFO', + coll: 'sightings', + documents: [ + { + description: 'Flying Saucer in the sky, numerous reports.', + where: 'Oklahoma', + // Last year. + year: `${new Date().getFullYear() - 1}`, + }, + { + description: 'Alien spaceship.', + where: 'Tennessee', + year: '2005', + }, + { + description: + 'Portal in the sky created by moving object, possibly just northern lights.', + where: 'Alaska', + year: '2020', + }, + { + description: 'Floating pineapple, likely northern lights.', + where: 'Alaska', + year: '2021', + }, + { + description: + 'Someone flying on a broomstick, sighters reported "It looks like Harry Potter".', + where: 'New York', + year: '2022', + }, + ], + }; +} + +const dynamicFixtures: { + db: string; + coll: string; + documents: Document[]; +}[] = [getDynamicDateFixture()]; + +export async function loadFixturesToDB({ + mongoClient, +}: { + mongoClient: MongoClient; +}): Promise { + const fixtureFiles = (await fs.readdir(__dirname, 'utf-8')).filter((f) => + f.endsWith('.json') + ); + + const fixtures: Fixtures = {}; + + // Load the static json fixtures. + for (const fixture of fixtureFiles) { + const fileContent = await fs.readFile( + path.join(__dirname, fixture), + 'utf-8' + ); + + const [db, coll] = fixture.split('.'); + + const ejson = EJSON.parse(fileContent); + + fixtures[db] = { [coll]: EJSON.serialize(ejson.data) }; + await mongoClient.db(db).collection(coll).insertMany(ejson.data); + + if (ejson.indexes) { + for (const index of ejson.indexes) { + await mongoClient.db(db).collection(coll).createIndex(index); + } + } + } + + // Load dynamic fixtures. + for (const { db, coll, documents } of dynamicFixtures) { + fixtures[db] = { [coll]: documents }; + await mongoClient.db(db).collection(coll).insertMany(documents); + } + + return fixtures; +} diff --git a/src/test/ai-accuracy-tests/test-results-page-styles.css b/src/test/ai-accuracy-tests/test-results-page-styles.css new file mode 100644 index 000000000..3dd8ccc78 --- /dev/null +++ b/src/test/ai-accuracy-tests/test-results-page-styles.css @@ -0,0 +1,27 @@ +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 24px; +} +th, +td { + border: 1px solid #ddd; + padding: 8px; + white-space: pre-wrap; +} +th { + background-color: #f2f2f2; + text-align: left; +} +tr:nth-child(even) { + background-color: #f9f9f9; +} +tr:hover td { + border-left-color: #777; + border-right-color: #777; +} +h1, +h2 { + font-family: Arial, sans-serif; + white-space: pre-wrap; +} diff --git a/src/test/ai-accuracy-tests/test-setup.ts b/src/test/ai-accuracy-tests/test-setup.ts new file mode 100644 index 000000000..07ce4a162 --- /dev/null +++ b/src/test/ai-accuracy-tests/test-setup.ts @@ -0,0 +1,30 @@ +import rewiremock from 'rewiremock'; + +const AssistantRole = 2; +const UserRole = 1; +const vscodeMock = { + LanguageModelChatMessageRole: { + Assistant: AssistantRole, + User: UserRole, + }, + LanguageModelChatMessage: { + Assistant: (content, name?: string) => ({ + name, + content, + role: AssistantRole, + }), + User: (content: string, name?: string) => ({ + content, + name, + role: UserRole, + }), + }, + window: { + createOutputChannel: () => {}, + }, +}; + +// Mock the 'vscode' module since we don't run the full vscode +// integration test setup for the ai-accuracy-tests as it's a bit slow. +rewiremock('vscode').with(vscodeMock); +rewiremock.enable(); diff --git a/src/test/runTest.ts b/src/test/runTest.ts index ab986a8a3..0a0afe6c9 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -17,41 +17,65 @@ async function startTestMongoDBServer() { }); } +let testMongoDBServer: MongoCluster; + +function cleanup() { + console.log('Stopping MongoDB server on port', TEST_DATABASE_PORT); + void testMongoDBServer?.close(); +} + async function main(): Promise { - const testMongoDBServer = await startTestMongoDBServer(); - - let failed = false; - - try { - // The folder containing the Extension Manifest package.json - // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath = path.join(__dirname, '../../'); - - // The path to test runner pased to --extensionTestsPath - const extensionTestsPath = path.join(__dirname, './suite/index'); - - // This is the workspace we open in our tests. - const testWorkspace = path.join(__dirname, '../../out/test'); - - // Download VS Code, unzip it and run the integration test - await runTests({ - version: 'insiders', // Download latest insiders. - extensionDevelopmentPath, - extensionTestsPath, - launchArgs: [testWorkspace, '--disable-extensions'], - }); - } catch (err) { - console.error('Failed to run tests:'); - console.error(err); - failed = true; - } finally { - console.log('Stopping MongoDB server on port', TEST_DATABASE_PORT); - await testMongoDBServer.close(); - } + testMongoDBServer = await startTestMongoDBServer(); - if (failed) { - process.exit(1); - } + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.join(__dirname, '../../'); + + // The path to test runner passed to --extensionTestsPath + const extensionTestsPath = path.join(__dirname, './suite/index'); + + // This is the workspace we open in our tests. + const testWorkspace = path.join(__dirname, '../../out/test'); + + // Download VS Code, unzip it and run the integration test + await runTests({ + version: 'insiders', // Download latest insiders. + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [testWorkspace, '--disable-extensions'], + }); + + cleanup(); } +process.once('SIGINT', () => { + console.log('Process was interrupted. Cleaning-up and exiting.'); + cleanup(); + process.kill(process.pid, 'SIGINT'); +}); + +process.once('SIGTERM', () => { + console.log('Process was terminated. Cleaning-up and exiting.'); + cleanup(); + process.kill(process.pid, 'SIGTERM'); +}); + +process.once('uncaughtException', (err: Error) => { + console.log('Uncaught exception. Cleaning-up and exiting.'); + cleanup(); + throw err; +}); + +process.on('unhandledRejection', (err: Error) => { + if (!err.message.match('Test run failed with code 1')?.[0]) { + // Log an unhandled exception when it's not the regular test failure. + // Test failures are logged in the test runner already so we avoid a generic message here. + console.log('Unhandled exception. Cleaning-up and exiting.'); + console.error(err.stack || err.message || err); + } + + cleanup(); + process.exitCode = 1; +}); + void main(); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index dc73f614b..9833e8055 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -1,3 +1,5 @@ +import sourceMapSupport from 'source-map-support'; +sourceMapSupport.install(); import Mocha from 'mocha'; import glob from 'glob'; import path from 'path'; From 934e8306a65d0a74bc495d0f2c3f5c7d3f3ea937 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Thu, 22 Aug 2024 12:00:26 +0200 Subject: [PATCH 10/45] feat: display only 10 items in clickable lists with the show more option VSCODE-589 (#785) --- src/connectionController.ts | 5 + src/explorer/connectionTreeItem.ts | 5 +- src/participant/participant.ts | 162 ++++++++++++++---- src/storage/connectionStorage.ts | 1 + .../suite/participant/participant.test.ts | 144 ++++++++++++---- 5 files changed, 247 insertions(+), 70 deletions(-) diff --git a/src/connectionController.ts b/src/connectionController.ts index b25da9201..9f62e0f20 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -435,7 +435,12 @@ export default class ConnectionController { this._currentConnectionId = connectionId; this._connectionAttempt = null; this._connectingConnectionId = null; + + this._connections[connectionId].lastUsed = new Date(); this.eventEmitter.emit(DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED); + await this._connectionStorage.saveConnection( + this._connections[connectionId] + ); // Send metrics to Segment this.sendTelemetry(dataService, connectionType); diff --git a/src/explorer/connectionTreeItem.ts b/src/explorer/connectionTreeItem.ts index 7eecddc72..680d89c2f 100644 --- a/src/explorer/connectionTreeItem.ts +++ b/src/explorer/connectionTreeItem.ts @@ -115,10 +115,7 @@ export default class ConnectionTreeItem } try { - const dbs = await dataService.listDatabases({ - nameOnly: true, - }); - + const dbs = await dataService.listDatabases(); return dbs.map((dbItem) => dbItem.name); } catch (error) { throw new Error( diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 5cd362a0e..7d4991d52 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -27,12 +27,19 @@ interface ChatResult extends vscode.ChatResult { }; } +interface NamespaceQuickPicks { + label: string; + data: string; +} + const DB_NAME_ID = 'DATABASE_NAME'; const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)\n`; const COL_NAME_ID = 'COLLECTION_NAME'; const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)`; +const MAX_MARKDOWN_LIST_LENGTH = 10; + export function parseForDatabaseAndCollectionName(text: string): { databaseName?: string; collectionName?: string; @@ -226,7 +233,7 @@ export default class ParticipantController { async connectWithParticipant(id?: string): Promise { if (!id) { - await this._connectionController.connectWithURI(); + await this._connectionController.changeActiveConnection(); } else { await this._connectionController.connectWithConnectionId(id); } @@ -259,22 +266,16 @@ export default class ParticipantController { return connName; } - // TODO (VSCODE-589): Evaluate the usability of displaying all existing connections in the list. - // Consider introducing a "recent connections" feature to display only a limited number of recent connections, - // with a "Show more" link that opens the Command Palette for access to the full list. - // If we implement this, the "Add new connection" link may become redundant, - // as this option is already available in the Command Palette dropdown. getConnectionsTree(): vscode.MarkdownString[] { return [ - this._createMarkdownLink({ - commandId: 'mdb.connectWithParticipant', - name: 'Add new connection', - }), ...this._connectionController .getSavedConnections() - .sort((connectionA: LoadedConnection, connectionB: LoadedConnection) => - (connectionA.name || '').localeCompare(connectionB.name || '') - ) + .sort((a, b) => { + const aTime = a.lastUsed ? new Date(a.lastUsed).getTime() : 0; + const bTime = b.lastUsed ? new Date(b.lastUsed).getTime() : 0; + return bTime - aTime; + }) + .slice(0, MAX_MARKDOWN_LIST_LENGTH) .map((conn: LoadedConnection) => this._createMarkdownLink({ commandId: 'mdb.connectWithParticipant', @@ -282,24 +283,96 @@ export default class ParticipantController { name: conn.name, }) ), + this._createMarkdownLink({ + commandId: 'mdb.connectWithParticipant', + name: 'Show more', + }), ]; } + async getDatabaseQuickPicks(): Promise { + const dataService = this._connectionController.getActiveDataService(); + if (!dataService) { + this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; + return []; + } + + try { + const databases = await dataService.listDatabases(); + return databases.map((db) => ({ + label: db.name, + data: db.name, + })); + } catch (error) { + return []; + } + } + + async _selectDatabaseWithCommandPalette(): Promise { + const databases = await this.getDatabaseQuickPicks(); + const selectedQuickPickItem = await vscode.window.showQuickPick(databases, { + placeHolder: 'Select a database...', + }); + return selectedQuickPickItem?.data; + } + async selectDatabaseWithParticipant(name: string): Promise { - this._databaseName = name; + if (!name) { + this._databaseName = await this._selectDatabaseWithCommandPalette(); + } else { + this._databaseName = name; + } + return vscode.commands.executeCommand('workbench.action.chat.open', { - query: `@MongoDB /query ${name}`, + query: `@MongoDB /query ${this._databaseName || ''}`, }); } + async getCollectionQuickPicks(): Promise { + if (!this._databaseName) { + return []; + } + + const dataService = this._connectionController.getActiveDataService(); + if (!dataService) { + this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; + return []; + } + + try { + const collections = await dataService.listCollections(this._databaseName); + return collections.map((db) => ({ + label: db.name, + data: db.name, + })); + } catch (error) { + return []; + } + } + + async _selectCollectionWithCommandPalette(): Promise { + const collections = await this.getCollectionQuickPicks(); + const selectedQuickPickItem = await vscode.window.showQuickPick( + collections, + { + placeHolder: 'Select a collection...', + } + ); + return selectedQuickPickItem?.data; + } + async selectCollectionWithParticipant(name: string): Promise { - this._collectionName = name; + if (!name) { + this._collectionName = await this._selectCollectionWithCommandPalette(); + } else { + this._collectionName = name; + } + return vscode.commands.executeCommand('workbench.action.chat.open', { - query: `@MongoDB /query ${name}`, + query: `@MongoDB /query ${this._collectionName || ''}`, }); } - // TODO (VSCODE-589): Display only 10 items in clickable lists with the show more option. async getDatabasesTree(): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { @@ -308,23 +381,30 @@ export default class ParticipantController { } try { - const databases = await dataService.listDatabases({ - nameOnly: true, - }); - return databases.map((db) => - this._createMarkdownLink({ - commandId: 'mdb.selectDatabaseWithParticipant', - query: db.name, - name: db.name, - }) - ); + const databases = await dataService.listDatabases(); + return [ + ...databases.slice(0, MAX_MARKDOWN_LIST_LENGTH).map((db) => + this._createMarkdownLink({ + commandId: 'mdb.selectDatabaseWithParticipant', + query: db.name, + name: db.name, + }) + ), + ...(databases.length > MAX_MARKDOWN_LIST_LENGTH + ? [ + this._createMarkdownLink({ + commandId: 'mdb.selectDatabaseWithParticipant', + name: 'Show more', + }), + ] + : []), + ]; } catch (error) { // Users can always do this manually when asked to provide a database name. return []; } } - // TODO (VSCODE-589): Display only 10 items in clickable lists with the show more option. async getCollectionTree(): Promise { if (!this._databaseName) { return []; @@ -338,13 +418,23 @@ export default class ParticipantController { try { const collections = await dataService.listCollections(this._databaseName); - return collections.map((coll) => - this._createMarkdownLink({ - commandId: 'mdb.selectCollectionWithParticipant', - query: coll.name, - name: coll.name, - }) - ); + return [ + ...collections.slice(0, MAX_MARKDOWN_LIST_LENGTH).map((coll) => + this._createMarkdownLink({ + commandId: 'mdb.selectCollectionWithParticipant', + query: coll.name, + name: coll.name, + }) + ), + ...(collections.length > MAX_MARKDOWN_LIST_LENGTH + ? [ + this._createMarkdownLink({ + commandId: 'mdb.selectCollectionWithParticipant', + name: 'Show more', + }), + ] + : []), + ]; } catch (error) { // Users can always do this manually when asked to provide a collection name. return []; diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index 56bb2f166..e233ad4d5 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -23,6 +23,7 @@ export interface StoreConnectionInfo { storageLocation: StorageLocation; secretStorageLocation?: SecretStorageLocationType; connectionOptions?: ConnectionOptions; + lastUsed?: Date; // Date and time when the connection was last used, i.e. connected with. } type StoreConnectionInfoWithConnectionOptions = StoreConnectionInfo & diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index cad2a596e..47cddbc6c 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -20,6 +20,15 @@ import { SecretStorageLocation, StorageLocation, } from '../../../storage/storageController'; +import type { LoadedConnection } from '../../../storage/connectionStorage'; + +const loadedConnection = { + id: 'id', + name: 'localhost', + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + connectionOptions: { connectionString: 'mongodb://localhost' }, +}; suite('Participant Controller Test Suite', function () { const extensionContextStub = new ExtensionContextStub(); @@ -121,16 +130,17 @@ suite('Participant Controller Test Suite', function () { suite('when not connected', function () { let connectWithConnectionIdStub; - let connectWithURIStub; + let changeActiveConnectionStub; + let getSavedConnectionsStub; beforeEach(function () { connectWithConnectionIdStub = sinon.stub( testParticipantController._connectionController, 'connectWithConnectionId' ); - connectWithURIStub = sinon.stub( + changeActiveConnectionStub = sinon.stub( testParticipantController._connectionController, - 'connectWithURI' + 'changeActiveConnection' ); sinon.replace( testParticipantController._connectionController, @@ -142,22 +152,16 @@ suite('Participant Controller Test Suite', function () { 'get', sinon.fake.returns(true) ); + getSavedConnectionsStub = sinon.stub(); sinon.replace( testParticipantController._connectionController, 'getSavedConnections', - () => [ - { - id: '123', - name: 'localhost', - storageLocation: StorageLocation.NONE, - secretStorageLocation: SecretStorageLocation.SecretStorage, - connectionOptions: { connectionString: 'mongodb://localhost' }, - }, - ] + getSavedConnectionsStub ); }); test('asks to connect', async function () { + getSavedConnectionsStub.returns([loadedConnection]); const chatRequestMock = { prompt: 'find all docs by a name example', command: 'query', @@ -173,15 +177,56 @@ suite('Participant Controller Test Suite', function () { expect(connectMessage).to.include( "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against." ); - const addNewConnectionMessage = - chatStreamStub.markdown.getCall(1).args[0]; - expect(addNewConnectionMessage.value).to.include( - '- Add new connection' + const listConnectionsMessage = chatStreamStub.markdown.getCall(1).args[0]; + expect(listConnectionsMessage.value).to.include( + '- localhost' + ); + const showMoreMessage = chatStreamStub.markdown.getCall(2).args[0]; + expect(showMoreMessage.value).to.include( + '- Show more' + ); + expect( + testParticipantController._chatResult?.metadata.responseContent + ).to.be.eql(undefined); + expect(testParticipantController._queryGenerationState).to.be.eql( + QUERY_GENERATION_STATE.ASK_TO_CONNECT + ); + }); + + test('shows only 10 connections with the show more option', async function () { + const connections: LoadedConnection[] = []; + for (let i = 0; i < 11; i++) { + connections.push({ + ...loadedConnection, + id: `${loadedConnection.id}${i}`, + name: `${loadedConnection.name}${i}`, + }); + } + getSavedConnectionsStub.returns(connections); + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const connectMessage = chatStreamStub.markdown.getCall(0).args[0]; + expect(connectMessage).to.include( + "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against." ); - const listConnectionsMessage = chatStreamStub.markdown.getCall(2).args[0]; + const listConnectionsMessage = chatStreamStub.markdown.getCall(1).args[0]; expect(listConnectionsMessage.value).to.include( - '- localhost' + '- localhost0' + ); + const showMoreMessage = chatStreamStub.markdown.getCall(11).args[0]; + expect(showMoreMessage.value).to.include( + '- Show more' ); + expect(chatStreamStub.markdown.callCount).to.be.eql(12); expect( testParticipantController._chatResult?.metadata.responseContent ).to.be.eql(undefined); @@ -191,6 +236,7 @@ suite('Participant Controller Test Suite', function () { }); test('handles empty connection name', async function () { + getSavedConnectionsStub.returns([loadedConnection]); const chatRequestMock = { prompt: 'find all docs by a name example', command: 'query', @@ -218,14 +264,13 @@ suite('Participant Controller Test Suite', function () { expect(emptyMessage).to.include( 'Please select a cluster to connect by clicking on an item in the connections list.' ); - const addNewConnectionMessage = - chatStreamStub.markdown.getCall(4).args[0]; - expect(addNewConnectionMessage.value).to.include( - '- Add new connection' - ); - const listConnectionsMessage = chatStreamStub.markdown.getCall(5).args[0]; + const listConnectionsMessage = chatStreamStub.markdown.getCall(4).args[0]; expect(listConnectionsMessage.value).to.include( - '- localhost' + '- localhost' + ); + const showMoreMessage = chatStreamStub.markdown.getCall(5).args[0]; + expect(showMoreMessage.value).to.include( + '- Show more' ); expect( testParticipantController._chatResult?.metadata.responseContent @@ -242,7 +287,7 @@ suite('Participant Controller Test Suite', function () { test('calls connect with uri for a new connection', async function () { await testParticipantController.connectWithParticipant(); - expect(connectWithURIStub).to.have.been.called; + expect(changeActiveConnectionStub).to.have.been.called; }); }); @@ -253,8 +298,34 @@ suite('Participant Controller Test Suite', function () { 'getActiveDataService', () => ({ - listDatabases: () => Promise.resolve([{ name: 'dbOne' }]), - listCollections: () => Promise.resolve([{ name: 'collOne' }]), + listDatabases: () => + Promise.resolve([ + { name: 'dbOne' }, + { name: 'customer' }, + { name: 'inventory' }, + { name: 'sales' }, + { name: 'employee' }, + { name: 'financialReports' }, + { name: 'productCatalog' }, + { name: 'projectTracker' }, + { name: 'user' }, + { name: 'analytics' }, + { name: '123' }, + ]), + listCollections: () => + Promise.resolve([ + { name: 'collOne' }, + { name: 'notifications' }, + { name: 'products' }, + { name: 'orders' }, + { name: 'categories' }, + { name: 'invoices' }, + { name: 'transactions' }, + { name: 'logs' }, + { name: 'messages' }, + { name: 'sessions' }, + { name: 'feedback' }, + ]), getMongoClientConnectionOptions: () => ({ url: TEST_DATABASE_URI, options: {}, @@ -391,6 +462,12 @@ suite('Participant Controller Test Suite', function () { expect(listDBsMessage.value).to.include( '- dbOne' ); + const showMoreDBsMessage = + chatStreamStub.markdown.getCall(11).args[0]; + expect(showMoreDBsMessage.value).to.include( + '- Show more' + ); + expect(chatStreamStub.markdown.callCount).to.be.eql(12); expect( testParticipantController._chatResult?.metadata.responseContent ).to.be.eql(undefined); @@ -410,14 +487,21 @@ suite('Participant Controller Test Suite', function () { 'dbOne' ); const askForCollMessage = - chatStreamStub.markdown.getCall(2).args[0]; + chatStreamStub.markdown.getCall(12).args[0]; expect(askForCollMessage).to.include( 'Which collection would you like to query within this database?' ); - const listCollsMessage = chatStreamStub.markdown.getCall(3).args[0]; + const listCollsMessage = + chatStreamStub.markdown.getCall(13).args[0]; expect(listCollsMessage.value).to.include( '- collOne' ); + const showMoreCollsMessage = + chatStreamStub.markdown.getCall(23).args[0]; + expect(showMoreCollsMessage.value).to.include( + '- Show more' + ); + expect(chatStreamStub.markdown.callCount).to.be.eql(24); expect( testParticipantController._chatResult?.metadata.responseContent ).to.be.eql(undefined); From 16277cb6217d6d352183b803c0f73936a237f33e Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 26 Aug 2024 13:42:03 +0200 Subject: [PATCH 11/45] chore: sync with main 26 08 (#791) * chore(testing): use source map support, improve test cleanup error handling VSCODE-593 (#784) * feat: show survey VSCODE-562 (#780) --------- Co-authored-by: Rhys Co-authored-by: Rhys Howell * fix: add telemetry events for survey VSCODE-595 (#787) * fix(editors): don't show connect code lens on regular js files VSCODE-538 (#789) --------- Co-authored-by: Rhys Co-authored-by: Paula Stachova Co-authored-by: Rhys Howell --- src/connectionController.ts | 4 + .../activeConnectionCodeLensProvider.ts | 36 ++--- src/editors/playgroundController.ts | 22 +-- src/mdbExtensionController.ts | 53 ++++++- src/storage/storageController.ts | 2 + src/telemetry/telemetryService.ts | 9 +- .../activeConnectionCodeLensProvider.test.ts | 49 ++++--- .../editors/playgroundController.test.ts | 8 -- ...aygroundSelectedCodeActionProvider.test.ts | 6 - .../language/languageServerController.test.ts | 5 - src/test/suite/mdbExtensionController.test.ts | 135 ++++++++++++++++++ .../views/webview-app/overview-page.test.tsx | 4 +- 12 files changed, 254 insertions(+), 79 deletions(-) diff --git a/src/connectionController.ts b/src/connectionController.ts index 9f62e0f20..a4aa8783a 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -806,6 +806,10 @@ export default class ConnectionController { this.eventEmitter.removeListener(eventType, listener); } + deactivate() { + this.eventEmitter.removeAllListeners(); + } + closeConnectionStringInput() { this._connectionStringInputCancellationToken?.cancel(); } diff --git a/src/editors/activeConnectionCodeLensProvider.ts b/src/editors/activeConnectionCodeLensProvider.ts index 62f227687..046b335eb 100644 --- a/src/editors/activeConnectionCodeLensProvider.ts +++ b/src/editors/activeConnectionCodeLensProvider.ts @@ -1,9 +1,10 @@ import * as vscode from 'vscode'; -import type { TextEditor } from 'vscode'; + import EXTENSION_COMMANDS from '../commands'; import type ConnectionController from '../connectionController'; import { isPlayground } from '../utils/playground'; import { getDBFromConnectionString } from '../utils/connection-string-db'; +import { DataServiceEventTypes } from '../connectionController'; export default class ActiveConnectionCodeLensProvider implements vscode.CodeLensProvider @@ -11,35 +12,29 @@ export default class ActiveConnectionCodeLensProvider _connectionController: ConnectionController; _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); - activeTextEditor?: TextEditor; + _activeConnectionChangedHandler: () => void; readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; constructor(connectionController: ConnectionController) { this._connectionController = connectionController; - this.activeTextEditor = vscode.window.activeTextEditor; vscode.workspace.onDidChangeConfiguration(() => { this._onDidChangeCodeLenses.fire(); }); - } - - setActiveTextEditor(activeTextEditor?: TextEditor) { - this.activeTextEditor = activeTextEditor; - this._onDidChangeCodeLenses.fire(); - } - - refresh(): void { - this._onDidChangeCodeLenses.fire(); - } - isPlayground(): boolean { - return isPlayground(this.activeTextEditor?.document.uri); + this._activeConnectionChangedHandler = () => { + this._onDidChangeCodeLenses.fire(); + }; + this._connectionController.addEventListener( + DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED, + this._activeConnectionChangedHandler + ); } - provideCodeLenses(): vscode.CodeLens[] { - if (!this.isPlayground()) { + provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] { + if (!isPlayground(document.uri)) { return []; } @@ -69,4 +64,11 @@ export default class ActiveConnectionCodeLensProvider return [codeLens]; } + + deactivate() { + this._connectionController.removeEventListener( + DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED, + this._activeConnectionChangedHandler + ); + } } diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 211682485..3e152ed0f 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -6,7 +6,6 @@ import vm from 'vm'; import os from 'os'; import transpiler from 'bson-transpilers'; -import type ActiveConnectionCodeLensProvider from './activeConnectionCodeLensProvider'; import type PlaygroundSelectedCodeActionProvider from './playgroundSelectedCodeActionProvider'; import type ConnectionController from '../connectionController'; import { DataServiceEventTypes } from '../connectionController'; @@ -129,12 +128,12 @@ export default class PlaygroundController { _isPartialRun = false; - private _activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider; private _playgroundResultViewColumn?: vscode.ViewColumn; private _playgroundResultTextDocument?: vscode.TextDocument; private _statusView: StatusView; private _playgroundResultViewProvider: PlaygroundResultProvider; private _participantController: ParticipantController; + private _activeConnectionChangedHandler: () => void; private _codeToEvaluate = ''; @@ -144,7 +143,6 @@ export default class PlaygroundController { telemetryService, statusView, playgroundResultViewProvider, - activeConnectionCodeLensProvider, exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider, participantController, @@ -154,7 +152,6 @@ export default class PlaygroundController { telemetryService: TelemetryService; statusView: StatusView; playgroundResultViewProvider: PlaygroundResultProvider; - activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider; exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; participantController: ParticipantController; @@ -165,17 +162,17 @@ export default class PlaygroundController { this._telemetryService = telemetryService; this._statusView = statusView; this._playgroundResultViewProvider = playgroundResultViewProvider; - this._activeConnectionCodeLensProvider = activeConnectionCodeLensProvider; this._exportToLanguageCodeLensProvider = exportToLanguageCodeLensProvider; this._playgroundSelectedCodeActionProvider = playgroundSelectedCodeActionProvider; this._participantController = participantController; + this._activeConnectionChangedHandler = () => { + void this._activeConnectionChanged(); + }; this._connectionController.addEventListener( DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED, - () => { - void this._activeConnectionChanged(); - } + this._activeConnectionChangedHandler ); const onDidChangeActiveTextEditor = ( @@ -195,9 +192,6 @@ export default class PlaygroundController { if (isPlaygroundEditor) { this._activeTextEditor = editor; - this._activeConnectionCodeLensProvider.setActiveTextEditor( - this._activeTextEditor - ); this._playgroundSelectedCodeActionProvider.setActiveTextEditor( this._activeTextEditor ); @@ -276,8 +270,6 @@ export default class PlaygroundController { const connectionId = this._connectionController.getActiveConnectionId(); let mongoClientOption; - this._activeConnectionCodeLensProvider.refresh(); - if (dataService && connectionId) { mongoClientOption = this._connectionController.getMongoClientConnectionOptions(); @@ -919,9 +911,7 @@ export default class PlaygroundController { deactivate(): void { this._connectionController.removeEventListener( DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED, - () => { - // No action is required after removing the listener. - } + this._activeConnectionChangedHandler ); } } diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index c6fee71d1..789ed84e5 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -33,7 +33,9 @@ import launchMongoShell from './commands/launchMongoShell'; import type SchemaTreeItem from './explorer/schemaTreeItem'; import { StatusView } from './views'; import { StorageController, StorageVariables } from './storage'; -import TelemetryService from './telemetry/telemetryService'; +import TelemetryService, { + TelemetryEventTypes, +} from './telemetry/telemetryService'; import type PlaygroundsTreeItem from './explorer/playgroundsTreeItem'; import PlaygroundResultProvider from './editors/playgroundResultProvider'; import WebviewController from './views/webviewController'; @@ -117,7 +119,6 @@ export default class MDBExtensionController implements vscode.Disposable { telemetryService: this._telemetryService, statusView: this._statusView, playgroundResultViewProvider: this._playgroundResultViewProvider, - activeConnectionCodeLensProvider: this._activeConnectionCodeLensProvider, exportToLanguageCodeLensProvider: this._exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: this._playgroundSelectedCodeActionProvider, @@ -157,6 +158,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommands(); this.showOverviewPageIfRecentlyInstalled(); + void this.showSurveyForEstablishedUsers(); } registerCommands = (): void => { @@ -887,6 +889,51 @@ export default class MDBExtensionController implements vscode.Disposable { } } + async showSurveyForEstablishedUsers(): Promise { + const surveyId = '9viN9wcbsC3zvHyg7'; + + const hasBeenShownSurveyAlready = + this._storageController.get(StorageVariables.GLOBAL_SURVEY_SHOWN) === + surveyId; + + // Show the survey when it hasn't been show to the + // user yet, and they have saved connections + // -> they haven't just started using this extension + if ( + hasBeenShownSurveyAlready || + !this._connectionStorage.hasSavedConnections() + ) { + return; + } + + const action = 'Share your thoughts'; + const text = 'How can we make the MongoDB extension better for you?'; + const link = 'https://forms.gle/9viN9wcbsC3zvHyg7'; + const result = await vscode.window.showInformationMessage( + text, + {}, + { + title: action, + } + ); + if (result?.title === action) { + void vscode.env.openExternal(vscode.Uri.parse(link)); + this._telemetryService.track(TelemetryEventTypes.SURVEY_CLICKED, { + survey_id: surveyId, + }); + } else { + this._telemetryService.track(TelemetryEventTypes.SURVEY_DISMISSED, { + survey_id: surveyId, + }); + } + + // whether action was taken or the prompt dismissed, we won't show this again + void this._storageController.update( + StorageVariables.GLOBAL_SURVEY_SHOWN, + surveyId + ); + } + async dispose(): Promise { await this.deactivate(); } @@ -902,5 +949,7 @@ export default class MDBExtensionController implements vscode.Disposable { this._telemetryService.deactivate(); this._editorsController.deactivate(); this._webviewController.deactivate(); + this._activeConnectionCodeLensProvider.deactivate(); + this._connectionController.deactivate(); } } diff --git a/src/storage/storageController.ts b/src/storage/storageController.ts index 7a2ba780b..e19eae149 100644 --- a/src/storage/storageController.ts +++ b/src/storage/storageController.ts @@ -6,6 +6,7 @@ import type { StoreConnectionInfo } from './connectionStorage'; export enum StorageVariables { // Only exists on globalState. GLOBAL_HAS_BEEN_SHOWN_INITIAL_VIEW = 'GLOBAL_HAS_BEEN_SHOWN_INITIAL_VIEW', + GLOBAL_SURVEY_SHOWN = 'GLOBAL_SURVEY_SHOWN', GLOBAL_SAVED_CONNECTIONS = 'GLOBAL_SAVED_CONNECTIONS', // Analytics user identify. GLOBAL_USER_ID = 'GLOBAL_USER_ID', @@ -51,6 +52,7 @@ interface StorageVariableContents { [StorageVariables.GLOBAL_USER_ID]: string; [StorageVariables.GLOBAL_ANONYMOUS_ID]: string; [StorageVariables.GLOBAL_HAS_BEEN_SHOWN_INITIAL_VIEW]: boolean; + [StorageVariables.GLOBAL_SURVEY_SHOWN]: string; [StorageVariables.GLOBAL_SAVED_CONNECTIONS]: ConnectionsFromStorage; [StorageVariables.WORKSPACE_SAVED_CONNECTIONS]: ConnectionsFromStorage; [StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE]: boolean; diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index a38b55a89..f3bcb1bf4 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -77,6 +77,10 @@ type ConnectionEditedTelemetryEventProperties = { success: boolean; }; +type SurveyActionProperties = { + survey_id: string; +}; + type SavedConnectionsLoadedProperties = { // Total number of connections saved on disk saved_connections: number; @@ -102,7 +106,8 @@ export type TelemetryEventProperties = | PlaygroundSavedTelemetryEventProperties | PlaygroundLoadedTelemetryEventProperties | KeytarSecretsMigrationFailedProperties - | SavedConnectionsLoadedProperties; + | SavedConnectionsLoadedProperties + | SurveyActionProperties; export enum TelemetryEventTypes { PLAYGROUND_CODE_EXECUTED = 'Playground Code Executed', @@ -120,6 +125,8 @@ export enum TelemetryEventTypes { PLAYGROUND_CREATED = 'Playground Created', KEYTAR_SECRETS_MIGRATION_FAILED = 'Keytar Secrets Migration Failed', SAVED_CONNECTIONS_LOADED = 'Saved Connections Loaded', + SURVEY_CLICKED = 'Survey link clicked', + SURVEY_DISMISSED = 'Survey prompt dismissed', } /** diff --git a/src/test/suite/editors/activeConnectionCodeLensProvider.test.ts b/src/test/suite/editors/activeConnectionCodeLensProvider.test.ts index 827c928f7..0af23e134 100644 --- a/src/test/suite/editors/activeConnectionCodeLensProvider.test.ts +++ b/src/test/suite/editors/activeConnectionCodeLensProvider.test.ts @@ -1,8 +1,9 @@ import * as vscode from 'vscode'; import { beforeEach, afterEach } from 'mocha'; -import chai from 'chai'; +import { expect } from 'chai'; import sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; +import path from 'path'; import ActiveConnectionCodeLensProvider from '../../../editors/activeConnectionCodeLensProvider'; import ConnectionController from '../../../connectionController'; @@ -12,8 +13,6 @@ import { ExtensionContextStub } from '../stubs'; import TelemetryService from '../../../telemetry/telemetryService'; import { TEST_DATABASE_URI } from '../dbTestHelper'; -const expect = chai.expect; - suite('Active Connection CodeLens Provider Test Suite', () => { const extensionContextStub = new ExtensionContextStub(); const testStorageController = new StorageController(extensionContextStub); @@ -42,19 +41,23 @@ suite('Active Connection CodeLens Provider Test Suite', () => { }); suite('the MongoDB playground in JS', () => { + const mockFileName = path.join('nonexistent', 'playground-test.mongodb.js'); + const mockDocumentUri = vscode.Uri.from({ + path: mockFileName, + scheme: 'untitled', + }); + const mockTextDoc: vscode.TextDocument = { + uri: mockDocumentUri, + } as Pick as vscode.TextDocument; + suite('user is not connected', () => { beforeEach(() => { - testCodeLensProvider.setActiveTextEditor( - vscode.window.activeTextEditor - ); const fakeShowQuickPick = sandbox.fake(); sandbox.replace(vscode.window, 'showQuickPick', fakeShowQuickPick); - const fakeIsPlayground = sandbox.fake.returns(true); - sandbox.replace(testCodeLensProvider, 'isPlayground', fakeIsPlayground); }); test('show disconnected message in code lenses', () => { - const codeLens = testCodeLensProvider.provideCodeLenses(); + const codeLens = testCodeLensProvider.provideCodeLenses(mockTextDoc); expect(codeLens).to.be.an('array'); expect(codeLens.length).to.be.equal(1); @@ -89,16 +92,11 @@ suite('Active Connection CodeLens Provider Test Suite', () => { } as unknown as DataService; testConnectionController.setActiveDataService(activeDataServiceStub); - testCodeLensProvider.setActiveTextEditor( - vscode.window.activeTextEditor - ); sandbox.replace( testConnectionController, 'getActiveConnectionName', sandbox.fake.returns('fakeName') ); - const fakeIsPlayground = sandbox.fake.returns(true); - sandbox.replace(testCodeLensProvider, 'isPlayground', fakeIsPlayground); }); test('show active connection in code lenses', () => { @@ -109,7 +107,7 @@ suite('Active Connection CodeLens Provider Test Suite', () => { url: TEST_DATABASE_URI, }) ); - const codeLens = testCodeLensProvider.provideCodeLenses(); + const codeLens = testCodeLensProvider.provideCodeLenses(mockTextDoc); expect(codeLens).to.be.an('array'); expect(codeLens.length).to.be.equal(1); @@ -131,7 +129,7 @@ suite('Active Connection CodeLens Provider Test Suite', () => { url: `${TEST_DATABASE_URI}/fakeDBName`, }) ); - const codeLens = testCodeLensProvider.provideCodeLenses(); + const codeLens = testCodeLensProvider.provideCodeLenses(mockTextDoc); expect(codeLens).to.be.an('array'); expect(codeLens.length).to.be.equal(1); expect(codeLens[0].command?.title).to.be.equal( @@ -147,16 +145,23 @@ suite('Active Connection CodeLens Provider Test Suite', () => { }); suite('the regular JS file', () => { + const mockFileName = path.join('nonexistent', 'playground-test.js'); + const mockDocumentUri = vscode.Uri.from({ + path: mockFileName, + scheme: 'untitled', + }); + const mockTextDoc: vscode.TextDocument = { + uri: mockDocumentUri, + } as Pick as vscode.TextDocument; + suite('user is not connected', () => { beforeEach(() => { const fakeShowQuickPick = sandbox.fake(); sandbox.replace(vscode.window, 'showQuickPick', fakeShowQuickPick); - const fakeIsPlayground = sandbox.fake.returns(false); - sandbox.replace(testCodeLensProvider, 'isPlayground', fakeIsPlayground); }); test('show not show the active connection code lenses', () => { - const codeLens = testCodeLensProvider.provideCodeLenses(); + const codeLens = testCodeLensProvider.provideCodeLenses(mockTextDoc); expect(codeLens).to.be.an('array'); expect(codeLens.length).to.be.equal(0); @@ -191,12 +196,10 @@ suite('Active Connection CodeLens Provider Test Suite', () => { 'getActiveConnectionName', sandbox.fake.returns('fakeName') ); - const fakeIsPlayground = sandbox.fake.returns(false); - sandbox.replace(testCodeLensProvider, 'isPlayground', fakeIsPlayground); }); - test('show not show the active connection code lensess', () => { - const codeLens = testCodeLensProvider.provideCodeLenses(); + test('show not show the active connection code lenses', () => { + const codeLens = testCodeLensProvider.provideCodeLenses(mockTextDoc); expect(codeLens).to.be.an('array'); expect(codeLens.length).to.be.equal(0); diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index c29343c69..fd3c7c910 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -8,7 +8,6 @@ import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import chaiAsPromised from 'chai-as-promised'; -import ActiveDBCodeLensProvider from '../../../editors/activeConnectionCodeLensProvider'; import PlaygroundSelectedCodeActionProvider from '../../../editors/playgroundSelectedCodeActionProvider'; import ConnectionController from '../../../connectionController'; import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensProvider'; @@ -51,7 +50,6 @@ suite('Playground Controller Test Suite', function () { let testConnectionController: ConnectionController; let testEditDocumentCodeLensProvider: EditDocumentCodeLensProvider; let testPlaygroundResultProvider: PlaygroundResultProvider; - let testActiveDBCodeLensProvider: ActiveDBCodeLensProvider; let testExportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; let testCodeActionProvider: PlaygroundSelectedCodeActionProvider; let languageServerControllerStub: LanguageServerController; @@ -80,9 +78,6 @@ suite('Playground Controller Test Suite', function () { testConnectionController, testEditDocumentCodeLensProvider ); - testActiveDBCodeLensProvider = new ActiveDBCodeLensProvider( - testConnectionController - ); testExportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); testCodeActionProvider = new PlaygroundSelectedCodeActionProvider(); @@ -100,7 +95,6 @@ suite('Playground Controller Test Suite', function () { telemetryService: testTelemetryService, statusView: testStatusView, playgroundResultViewProvider: testPlaygroundResultProvider, - activeConnectionCodeLensProvider: testActiveDBCodeLensProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, participantController: testParticipantController, @@ -356,7 +350,6 @@ suite('Playground Controller Test Suite', function () { telemetryService: testTelemetryService, statusView: testStatusView, playgroundResultViewProvider: testPlaygroundResultProvider, - activeConnectionCodeLensProvider: testActiveDBCodeLensProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, @@ -375,7 +368,6 @@ suite('Playground Controller Test Suite', function () { telemetryService: testTelemetryService, statusView: testStatusView, playgroundResultViewProvider: testPlaygroundResultProvider, - activeConnectionCodeLensProvider: testActiveDBCodeLensProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, diff --git a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts index d1ac86c1e..9b5a0d421 100644 --- a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts +++ b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts @@ -3,7 +3,6 @@ import { beforeEach, afterEach } from 'mocha'; import chai from 'chai'; import sinon from 'sinon'; -import ActiveConnectionCodeLensProvider from '../../../editors/activeConnectionCodeLensProvider'; import ExportToLanguageCodeLensProvider from '../../../editors/exportToLanguageCodeLensProvider'; import PlaygroundSelectedCodeActionProvider from '../../../editors/playgroundSelectedCodeActionProvider'; import { LanguageServerController } from '../../../language'; @@ -77,10 +76,6 @@ suite('Playground Selected CodeAction Provider Test Suite', function () { TEST_DATABASE_URI ); - const activeConnectionCodeLensProvider = - new ActiveConnectionCodeLensProvider( - mdbTestExtension.testExtensionController._connectionController - ); const testExportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); @@ -96,7 +91,6 @@ suite('Playground Selected CodeAction Provider Test Suite', function () { playgroundResultViewProvider: mdbTestExtension.testExtensionController ._playgroundResultViewProvider, - activeConnectionCodeLensProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index b86f2f975..5c0a7b4aa 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -8,7 +8,6 @@ import type { SinonStub } from 'sinon'; import type { DataService } from 'mongodb-data-service'; import chaiAsPromised from 'chai-as-promised'; -import ActiveDBCodeLensProvider from '../../../editors/activeConnectionCodeLensProvider'; import PlaygroundSelectedCodeActionProvider from '../../../editors/playgroundSelectedCodeActionProvider'; import ConnectionController from '../../../connectionController'; import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensProvider'; @@ -52,9 +51,6 @@ suite('Language Server Controller Test Suite', () => { testConnectionController, testEditDocumentCodeLensProvider ); - const testActiveDBCodeLensProvider = new ActiveDBCodeLensProvider( - testConnectionController - ); const testExportToLanguageCodeLensProvider = new ExportToLanguageCodeLensProvider(); const testCodeActionProvider = new PlaygroundSelectedCodeActionProvider(); @@ -79,7 +75,6 @@ suite('Language Server Controller Test Suite', () => { telemetryService: testTelemetryService, statusView: testStatusView, playgroundResultViewProvider: testPlaygroundResultProvider, - activeConnectionCodeLensProvider: testActiveDBCodeLensProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, participantController: testParticipantController, diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 9fc872a19..e887485b9 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -175,6 +175,7 @@ suite('MDBExtensionController Test Suite', function () { let fakeActiveConnectionId: SinonSpy; let showErrorMessageStub: SinonStub; let fakeCreatePlaygroundFileWithContent: SinonSpy; + let openExternalStub: SinonStub; const sandbox = sinon.createSandbox(); @@ -183,6 +184,7 @@ suite('MDBExtensionController Test Suite', function () { vscode.window, 'showInformationMessage' ); + openExternalStub = sandbox.stub(vscode.env, 'openExternal'); openTextDocumentStub = sandbox.stub(vscode.workspace, 'openTextDocument'); fakeActiveConnectionId = sandbox.fake.returns('tasty_sandwhich'); sandbox.replace( @@ -1717,5 +1719,138 @@ suite('MDBExtensionController Test Suite', function () { }); }); }); + + suite('survey prompt', function () { + suite( + "when a user hasn't been shown the survey prompt yet, and they have connections saved", + () => { + [ + { + description: 'clicked the button', + value: { title: 'Share your thoughts' }, + }, + { description: 'dismissed', value: undefined }, + ].forEach((reaction) => { + suite(`user ${reaction.description}`, () => { + let connectionsUpdateStub: SinonStub; + let uriParseStub: SinonStub; + beforeEach(async () => { + showInformationMessageStub.resolves(reaction.value); + openExternalStub.resolves(undefined); + sandbox.replace( + mdbTestExtension.testExtensionController._storageController, + 'get', + sandbox.fake.returns(undefined) + ); + sandbox.replace( + mdbTestExtension.testExtensionController._connectionStorage, + 'hasSavedConnections', + sandbox.fake.returns(true) + ); + connectionsUpdateStub = sandbox.stub( + mdbTestExtension.testExtensionController._storageController, + 'update' + ); + uriParseStub = sandbox.stub(vscode.Uri, 'parse'); + connectionsUpdateStub.resolves(undefined); + await mdbTestExtension.testExtensionController.showSurveyForEstablishedUsers(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + test('they are shown the survey prompt', () => { + assert(showInformationMessageStub.called); + assert.strictEqual( + showInformationMessageStub.firstCall.args[0], + 'How can we make the MongoDB extension better for you?' + ); + }); + + test('the link was open if and only if they click the button', () => { + if (reaction.value === undefined) { + assert(openExternalStub.notCalled); + } + if (reaction.value) { + assert(openExternalStub.called); + assert(uriParseStub.called); + assert.strictEqual( + uriParseStub.firstCall.args[0], + 'https://forms.gle/9viN9wcbsC3zvHyg7' + ); + } + }); + + test("it sets that they've been shown the survey", () => { + assert(connectionsUpdateStub.called); + assert.strictEqual( + connectionsUpdateStub.firstCall.args[0], + StorageVariables.GLOBAL_SURVEY_SHOWN + ); + assert.strictEqual( + connectionsUpdateStub.firstCall.args[1], + '9viN9wcbsC3zvHyg7' + ); + }); + }); + }); + } + ); + + suite('when a user has been shown the survey prompt already', () => { + let connectionsUpdateStub: SinonStub; + beforeEach(() => { + sandbox.replace( + mdbTestExtension.testExtensionController._storageController, + 'get', + sandbox.fake.returns('9viN9wcbsC3zvHyg7') // survey has been shown + ); + sandbox.replace( + mdbTestExtension.testExtensionController._connectionStorage, + 'hasSavedConnections', + sandbox.fake.returns(true) + ); + connectionsUpdateStub = sandbox.stub( + mdbTestExtension.testExtensionController._storageController, + 'update' + ); + connectionsUpdateStub.resolves(undefined); + + void mdbTestExtension.testExtensionController.showSurveyForEstablishedUsers(); + }); + + test('they are not shown the survey prompt', () => { + assert(showInformationMessageStub.notCalled); + }); + }); + + suite('when a has no connections saved', () => { + let connectionsUpdateStub: SinonStub; + beforeEach(() => { + sandbox.replace( + mdbTestExtension.testExtensionController._storageController, + 'get', + sandbox.fake.returns(undefined) + ); + sandbox.replace( + mdbTestExtension.testExtensionController._connectionStorage, + 'hasSavedConnections', + sandbox.fake.returns(false) // no connections yet - this might be the first install + ); + connectionsUpdateStub = sandbox.stub( + mdbTestExtension.testExtensionController._storageController, + 'update' + ); + connectionsUpdateStub.resolves(undefined); + + void mdbTestExtension.testExtensionController.showSurveyForEstablishedUsers(); + }); + + test('they are not shown the survey prompt', () => { + assert(showInformationMessageStub.notCalled); + }); + }); + }); }); }); diff --git a/src/test/suite/views/webview-app/overview-page.test.tsx b/src/test/suite/views/webview-app/overview-page.test.tsx index 3dc9abbbf..459e2d75b 100644 --- a/src/test/suite/views/webview-app/overview-page.test.tsx +++ b/src/test/suite/views/webview-app/overview-page.test.tsx @@ -39,7 +39,9 @@ describe('OverviewPage test suite', function () { describe('Connection Form', function () { // Rendering the connection form takes ~4 seconds, so we need to increase the timeout. // Not sure on the cause of this slowdown, it could be animation based. - this.timeout(20000); + // Without this it's flaky on mac CI. + // TODO(COMPASS-7762): Once we update the connection form this may be able to go away. + this.timeout(25000); it('is able to open and close the new connection form', async function () { render(); From 7a9ba2aba3b04fac27c5d7e7c20b619e6eddc259 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Tue, 27 Aug 2024 14:56:55 +0200 Subject: [PATCH 12/45] feat: send schema to the model for better results VSCODE-581 (#792) * feat: send schema to the model for better results VSCODE-581 * refactor: change state in fetchCollectionSchema --- src/participant/participant.ts | 96 ++++++++++++++--- src/participant/prompts/query.ts | 12 ++- src/participant/schema.ts | 102 ++++++++++++++++++ .../suite/participant/participant.test.ts | 49 +++++++-- 4 files changed, 233 insertions(+), 26 deletions(-) create mode 100644 src/participant/schema.ts diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 7d4991d52..01d4c1742 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { getSimplifiedSchema } from 'mongodb-schema'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; @@ -10,6 +11,7 @@ import { GenericPrompt } from './prompts/generic'; import { CHAT_PARTICIPANT_ID, CHAT_PARTICIPANT_MODEL } from './constants'; import { QueryPrompt } from './prompts/query'; import { NamespacePrompt } from './prompts/namespace'; +import { SchemaFormatter } from './schema'; const log = createLogger('participant'); @@ -18,9 +20,11 @@ export enum QUERY_GENERATION_STATE { ASK_TO_CONNECT = 'ASK_TO_CONNECT', ASK_FOR_DATABASE_NAME = 'ASK_FOR_DATABASE_NAME', ASK_FOR_COLLECTION_NAME = 'ASK_FOR_COLLECTION_NAME', - READY_TO_GENERATE_QUERY = 'READY_TO_GENERATE_QUERY', + FETCH_SCHEMA = 'FETCH_SCHEMA', } +const NUM_DOCUMENTS_TO_SAMPLE = 4; + interface ChatResult extends vscode.ChatResult { metadata: { responseContent?: string; @@ -67,6 +71,7 @@ export default class ParticipantController { _chatResult?: ChatResult; _databaseName?: string; _collectionName?: string; + _schema?: string; constructor({ connectionController, @@ -79,6 +84,26 @@ export default class ParticipantController { this._storageController = storageController; } + _setDatabaseName(name: string | undefined) { + if ( + this._queryGenerationState === QUERY_GENERATION_STATE.DEFAULT && + this._databaseName !== name + ) { + this._queryGenerationState = QUERY_GENERATION_STATE.FETCH_SCHEMA; + } + this._databaseName = name; + } + + _setCollectionName(name: string | undefined) { + if ( + this._queryGenerationState === QUERY_GENERATION_STATE.DEFAULT && + this._collectionName !== name + ) { + this._queryGenerationState = QUERY_GENERATION_STATE.FETCH_SCHEMA; + } + this._collectionName = name; + } + createParticipant(context: vscode.ExtensionContext) { // Chat participants appear as top-level options in the chat input // when you type `@`, and can contribute sub-commands in the chat input @@ -318,9 +343,10 @@ export default class ParticipantController { async selectDatabaseWithParticipant(name: string): Promise { if (!name) { - this._databaseName = await this._selectDatabaseWithCommandPalette(); + const selectedName = await this._selectDatabaseWithCommandPalette(); + this._setDatabaseName(selectedName); } else { - this._databaseName = name; + this._setDatabaseName(name); } return vscode.commands.executeCommand('workbench.action.chat.open', { @@ -363,9 +389,10 @@ export default class ParticipantController { async selectCollectionWithParticipant(name: string): Promise { if (!name) { - this._collectionName = await this._selectCollectionWithCommandPalette(); + const selectedName = await this._selectCollectionWithCommandPalette(); + this._setCollectionName(selectedName); } else { - this._collectionName = name; + this._setCollectionName(name); } return vscode.commands.executeCommand('workbench.action.chat.open', { @@ -448,8 +475,8 @@ export default class ParticipantController { if (isNewChat) { this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; this._chatResult = undefined; - this._databaseName = undefined; - this._collectionName = undefined; + this._setDatabaseName(undefined); + this._setCollectionName(undefined); } } @@ -468,7 +495,7 @@ export default class ParticipantController { this._queryGenerationState === QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME ) { - this._databaseName = prompt; + this._setDatabaseName(prompt); if (!this._collectionName) { this._queryGenerationState = QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME; @@ -481,14 +508,13 @@ export default class ParticipantController { this._queryGenerationState === QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME ) { - this._collectionName = prompt; + this._setCollectionName(prompt); if (!this._databaseName) { this._queryGenerationState = QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME; return true; } - this._queryGenerationState = - QUERY_GENERATION_STATE.READY_TO_GENERATE_QUERY; + this._queryGenerationState = QUERY_GENERATION_STATE.FETCH_SCHEMA; return false; } @@ -522,12 +548,11 @@ export default class ParticipantController { responseContentWithNamespace ); - this._databaseName = namespace.databaseName || this._databaseName; - this._collectionName = namespace.collectionName || this._collectionName; + this._setDatabaseName(namespace.databaseName || this._databaseName); + this._setCollectionName(namespace.collectionName || this._collectionName); if (namespace.databaseName && namespace.collectionName) { - this._queryGenerationState = - QUERY_GENERATION_STATE.READY_TO_GENERATE_QUERY; + this._queryGenerationState = QUERY_GENERATION_STATE.FETCH_SCHEMA; return false; } @@ -590,6 +615,41 @@ export default class ParticipantController { return true; } + _shouldFetchCollectionSchema(): boolean { + return this._queryGenerationState === QUERY_GENERATION_STATE.FETCH_SCHEMA; + } + + async _fetchCollectionSchema(abortSignal?: AbortSignal): Promise { + if (this._queryGenerationState === QUERY_GENERATION_STATE.FETCH_SCHEMA) { + this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; + } + + const dataService = this._connectionController.getActiveDataService(); + if (!dataService || !this._databaseName || !this._collectionName) { + return; + } + + try { + const sampleDocuments = + (await dataService?.sample?.( + `${this._databaseName}.${this._collectionName}`, + { + query: {}, + size: NUM_DOCUMENTS_TO_SAMPLE, + }, + { promoteValues: false }, + { + abortSignal, + } + )) || []; + + const schema = await getSimplifiedSchema(sampleDocuments); + this._schema = new SchemaFormatter().format(schema); + } catch (err: any) { + this._schema = undefined; + } + } + // @MongoDB /query find all documents where the "address" has the word Broadway in it. async handleQueryRequest( request: vscode.ChatRequest, @@ -621,13 +681,17 @@ export default class ParticipantController { abortController.abort(); }); + if (this._shouldFetchCollectionSchema()) { + await this._fetchCollectionSchema(abortController.signal); + } + const messages = QueryPrompt.buildMessages({ request, context, databaseName: this._databaseName, collectionName: this._collectionName, + schema: this._schema, }); - const responseContent = await this.getChatResponseContent({ messages, stream, diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index 09e2fc8ba..58037a58b 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -6,9 +6,11 @@ export class QueryPrompt { static getAssistantPrompt({ databaseName = 'mongodbVSCodeCopilotDB', collectionName = 'test', + schema, }: { databaseName?: string; collectionName?: string; + schema?: string; }): vscode.LanguageModelChatMessage { const prompt = `You are a MongoDB expert. @@ -38,6 +40,12 @@ db.getCollection('').find({ Database name: ${databaseName} Collection name: ${collectionName} +${ + schema + ? `Collection schema: +${schema}` + : '' +} MongoDB command to specify database: use(''); @@ -61,6 +69,7 @@ Concisely explain the code snippet you have generated.`; request, databaseName, collectionName, + schema, }: { request: { prompt: string; @@ -68,9 +77,10 @@ Concisely explain the code snippet you have generated.`; context: vscode.ChatContext; databaseName?: string; collectionName?: string; + schema?: string; }): vscode.LanguageModelChatMessage[] { const messages = [ - QueryPrompt.getAssistantPrompt({ databaseName, collectionName }), + QueryPrompt.getAssistantPrompt({ databaseName, collectionName, schema }), ...getHistoryMessages({ context }), QueryPrompt.getUserPrompt(request.prompt), ]; diff --git a/src/participant/schema.ts b/src/participant/schema.ts new file mode 100644 index 000000000..faf1e7fb8 --- /dev/null +++ b/src/participant/schema.ts @@ -0,0 +1,102 @@ +import type { + SimplifiedSchema, + SimplifiedSchemaArrayType, + SimplifiedSchemaDocumentType, + SimplifiedSchemaType, +} from 'mongodb-schema'; + +const PROPERTY_REGEX = '^[a-zA-Z_$][0-9a-zA-Z_$]*$'; + +export class SchemaFormatter { + static getSchemaFromTypes(pInput: SimplifiedSchema): string { + return new SchemaFormatter().format(pInput); + } + + schemaString = ''; + + format(pInitial: SimplifiedSchema): string { + this.processDocumentType('', pInitial); + return this.schemaString; + } + + private processSchemaTypeList( + prefix: string, + pTypes: SimplifiedSchemaType[] + ) { + if (pTypes.length !== 0) { + this.processSchemaType(prefix, pTypes[0]); + } + } + + private processSchemaType(prefix: string, pType: SimplifiedSchemaType) { + const bsonType = pType.bsonType; + if (bsonType === 'Document') { + const fields = (pType as SimplifiedSchemaDocumentType).fields; + + if (Object.keys(fields).length === 0) { + this.addToFormattedSchemaString(prefix + ': Document'); + return; + } + + this.processDocumentType(prefix, fields); + return; + } + + if (bsonType === 'Array') { + const types = (pType as SimplifiedSchemaArrayType).types; + + if (types.length === 0) { + this.addToFormattedSchemaString(prefix + ': ' + 'Array'); + return; + } + + const firstType = types[0].bsonType; + if (firstType !== 'Array' && firstType !== 'Document') { + this.addToFormattedSchemaString( + prefix + ': ' + 'Array<' + firstType + '>' + ); + return; + } + + // Array of documents or arrays. + // We only use the first type. + this.processSchemaType(prefix + '[]', types[0]); + return; + } + + this.addToFormattedSchemaString(prefix + ': ' + bsonType); + } + + private processDocumentType(prefix: string, pDoc: SimplifiedSchema) { + if (!pDoc) { + return; + } + + Object.keys(pDoc).forEach((key) => { + const keyAsString = this.getPropAsString(key); + this.processSchemaTypeList( + prefix + (prefix.length === 0 ? '' : '.') + keyAsString, + pDoc[key]?.types + ); + }); + } + + getPropAsString(pProp: string): string { + if (pProp.match(PROPERTY_REGEX)) { + return pProp; + } + + try { + return JSON.stringify(pProp); + } catch (e) { + return pProp; + } + } + + addToFormattedSchemaString(fieldAndType: string) { + if (this.schemaString.length > 0) { + this.schemaString += '\n'; + } + this.schemaString += fieldAndType; + } +} diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 47cddbc6c..8f258ccf6 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -44,6 +44,7 @@ suite('Participant Controller Test Suite', function () { let chatContextStub; let chatStreamStub; let chatTokenStub; + let sendRequestStub; beforeEach(function () { testStorageController = new StorageController(extensionContextStub); @@ -78,6 +79,14 @@ suite('Participant Controller Test Suite', function () { onCancellationRequested: () => {}, }; // The model returned by vscode.lm.selectChatModels is always undefined in tests. + sendRequestStub = sinon.fake.resolves({ + text: [ + '```javascript\n' + + "use('dbOne');\n" + + "db.getCollection('collOne').find({ name: 'example' });\n" + + '```', + ], + }); sinon.replace( vscode.lm, 'selectChatModels', @@ -90,15 +99,7 @@ suite('Participant Controller Test Suite', function () { name: 'GPT 4o (date)', maxInputTokens: 16211, countTokens: () => {}, - sendRequest: () => - Promise.resolve({ - text: [ - '```javascript\n' + - "use('dbOne');\n" + - "db.getCollection('collOne').find({ name: 'example' });\n" + - '```', - ], - }), + sendRequest: sendRequestStub, }, ]) ); @@ -330,6 +331,13 @@ suite('Participant Controller Test Suite', function () { url: TEST_DATABASE_URI, options: {}, }), + sample: () => + Promise.resolve([ + { + _id: '66b3408a60da951fc354743e', + field: { subField: '66b3408a60da951fc354743e' }, + }, + ]), once: sinon.stub(), } as unknown as DataService) ); @@ -436,6 +444,29 @@ suite('Participant Controller Test Suite', function () { "db.getCollection('collOne').find({ name: 'example' });" ); }); + + test('includes a collection schema', async function () { + sinon + .stub(testParticipantController, '_queryGenerationState') + .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const messages = sendRequestStub.firstCall.args[0]; + expect(messages[0].content).to.include( + 'Collection schema:\n' + + '_id: String\n' + + 'field.subField: String\n' + ); + }); }); suite('unknown namespace', function () { From d8f49bd896188da4c0954513b8fe5e89ad7b1df3 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Mon, 2 Sep 2024 10:31:44 +0200 Subject: [PATCH 13/45] chore: sync with main 02 09 (#803) * chore(testing): use source map support, improve test cleanup error handling VSCODE-593 (#784) * feat: show survey VSCODE-562 (#780) --------- Co-authored-by: Rhys Co-authored-by: Rhys Howell * fix: add telemetry events for survey VSCODE-595 (#787) * fix(editors): don't show connect code lens on regular js files VSCODE-538 (#789) * fix(deps): bump shell and data-service deps VSCODE-564 (#794) * fix: clean-up webpack config VSCODE-597 (#795) * Update changelog (#796) Co-authored-by: mcasimir <334881+mcasimir@users.noreply.github.com> * Update changelog (#797) Co-authored-by: alenakhineika <16307679+alenakhineika@users.noreply.github.com> * fix: cannot find module electron VSCODE-598 (#799) * Update changelog (#801) Co-authored-by: mcasimir <334881+mcasimir@users.noreply.github.com> * Update changelog (#802) Co-authored-by: alenakhineika <16307679+alenakhineika@users.noreply.github.com> --------- Co-authored-by: Rhys Co-authored-by: Paula Stachova Co-authored-by: Rhys Howell Co-authored-by: Anna Henningsen Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: mcasimir <334881+mcasimir@users.noreply.github.com> --- CHANGELOG.md | 56 +- package-lock.json | 2282 +++++++++---------- package.json | 12 +- src/test/suite/connectionController.test.ts | 4 +- webpack.config.js | 6 +- 5 files changed, 1152 insertions(+), 1208 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 597a75947..026fe8187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## [v1.8.1](https://github.com/mongodb-js/vscode/releases/tag/v1.8.1) - 2024-09-02 + +## What's Changed +* fix: cannot find module electron VSCODE-598 by @alenakhineika in https://github.com/mongodb-js/vscode/pull/799 + + +**Full Changelog**: https://github.com/mongodb-js/vscode/compare/v1.8.0...v1.8.1 + + +## [v1.8.0](https://github.com/mongodb-js/vscode/releases/tag/v1.8.0) - 2024-08-29 + +## What's Changed +* feat: show survey VSCODE-562 by @paula-stacho in https://github.com/mongodb-js/vscode/pull/780 +* fix(editors): don't show connect code lens on regular js files VSCODE-538 by @Anemy in https://github.com/mongodb-js/vscode/pull/789 + +**Full Changelog**: https://github.com/mongodb-js/vscode/compare/v1.7.0...v1.8.0 + + ## [v1.7.0](https://github.com/mongodb-js/vscode/releases/tag/v1.7.0) - 2024-08-01 ## What's Changed @@ -408,41 +426,3 @@ To dig deeper please feel free to follow the links mentioned below: - Fixed code lenses for partial playground executions sometimes being missing (VSCODE-226, #243) -## [v0.3.0](https://github.com/mongodb-js/vscode/releases/tag/v0.3.0) - 2020-11-24 - -### Added - -- Added a resources panel to the overview page (VSCODE-168, #208) -- Added a button to change currently connected connection name in overview page (VSCODE-189, #191) - -### Changed - -- Moved playground output to the editor, logs remain in the output section (VSCODE-177, #198) -- Playground results are now displayed in EJSON or text (VSCODE-203, #206) -- Connect commands route to overview page instead of directly to connection form (VSCODE-170, #210) -- Connection form shows in modal in overview page (VSCODE-173, #190) -- Index creation template now uses `createIndex` instead of `ensureIndex` (#205) - -### Fixed - -- Fix x509 username being required in connection string building and parsing (VSCODE-202, #203) -- Fix viewing documents Binary UUID _ids (VSCODE-118, #213) -- Fix opening mongodb shell on windows with git bash (VSCODE-200, #201) -- Fix opening mongodb shell on windows with an ssl file with a space in it (#201) -- Fix password encoding of connection strings when copying connection string and opening in mongodb shell (VSCODE-198, #207) - -### Removed - -- Removed custom font in overview page (#192) -- Removed sql pipeline stage operator (#211) - - -## [v0.2.1](https://github.com/mongodb-js/vscode/releases/tag/0.2.1) - 2020-10-20 - -### Added - -- Added the ability to use print() and console.log() in playgrounds [#184](https://github.com/mongodb-js/vscode/pull/184) -- Added an overview page (more to come on this in future versions) [#178](https://github.com/mongodb-js/vscode/pull/178) -- Added a tooltip to fields in a collection's schema to show types found in the sampling for that field [#179](https://github.com/mongodb-js/vscode/pull/179) - - diff --git a/package-lock.json b/package-lock.json index d7bec7616..f40460af4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,10 @@ "@mongodb-js/connection-form": "^1.22.2", "@mongodb-js/connection-info": "^0.1.2", "@mongodb-js/mongodb-constants": "^0.10.2", - "@mongosh/browser-runtime-electron": "^2.2.15", - "@mongosh/i18n": "^2.2.15", - "@mongosh/service-provider-server": "^2.2.15", - "@mongosh/shell-api": "^2.2.15", + "@mongosh/browser-runtime-electron": "^2.3.0", + "@mongosh/i18n": "^2.3.0", + "@mongosh/service-provider-server": "^2.3.0", + "@mongosh/shell-api": "^2.3.0", "@segment/analytics-node": "^1.3.0", "bson": "^6.8.0", "bson-transpilers": "^2.2.0", @@ -32,9 +32,9 @@ "mongodb-build-info": "^1.7.2", "mongodb-cloud-info": "^2.1.4", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.21.1", + "mongodb-data-service": "^22.23.0", "mongodb-log-writer": "^1.4.2", - "mongodb-query-parser": "^4.1.3", + "mongodb-query-parser": "^4.2.2", "mongodb-schema": "^12.2.0", "numeral": "^2.0.6", "react": "^18.3.1", @@ -162,6 +162,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", @@ -176,6 +177,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -187,6 +189,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" @@ -199,6 +202,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" @@ -211,6 +215,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", @@ -224,6 +229,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" } @@ -232,6 +238,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", @@ -242,6 +249,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -253,6 +261,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" @@ -265,6 +274,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" @@ -274,49 +284,50 @@ } }, "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.600.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.600.0.tgz", - "integrity": "sha512-8dYsnDLiD0rjujRiZZl0E57heUkHqMSFZHBi0YMs57SM8ODPxK3tahwDYZtS7bqanvFKZwGy+o9jIcij7jBOlA==", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.621.0.tgz", + "integrity": "sha512-FpXia5qFf6ijcNDWenVq+mP9r1LbiW/+52i9wrv2+Afi6Nn1ROf8W7St8WvE9TEZ3t78y+vis4CwqfGts+uiKA==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.600.0", - "@aws-sdk/client-sts": "3.600.0", - "@aws-sdk/core": "3.598.0", - "@aws-sdk/credential-provider-node": "3.600.0", - "@aws-sdk/middleware-host-header": "3.598.0", - "@aws-sdk/middleware-logger": "3.598.0", - "@aws-sdk/middleware-recursion-detection": "3.598.0", - "@aws-sdk/middleware-user-agent": "3.598.0", - "@aws-sdk/region-config-resolver": "3.598.0", - "@aws-sdk/types": "3.598.0", - "@aws-sdk/util-endpoints": "3.598.0", - "@aws-sdk/util-user-agent-browser": "3.598.0", - "@aws-sdk/util-user-agent-node": "3.598.0", - "@smithy/config-resolver": "^3.0.2", - "@smithy/core": "^2.2.1", - "@smithy/fetch-http-handler": "^3.0.2", - "@smithy/hash-node": "^3.0.1", - "@smithy/invalid-dependency": "^3.0.1", - "@smithy/middleware-content-length": "^3.0.1", - "@smithy/middleware-endpoint": "^3.0.2", - "@smithy/middleware-retry": "^3.0.4", - "@smithy/middleware-serde": "^3.0.1", - "@smithy/middleware-stack": "^3.0.1", - "@smithy/node-config-provider": "^3.1.1", - "@smithy/node-http-handler": "^3.0.1", - "@smithy/protocol-http": "^4.0.1", - "@smithy/smithy-client": "^3.1.2", - "@smithy/types": "^3.1.0", - "@smithy/url-parser": "^3.0.1", + "@aws-sdk/client-sso-oidc": "3.621.0", + "@aws-sdk/client-sts": "3.621.0", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.4", - "@smithy/util-defaults-mode-node": "^3.0.4", - "@smithy/util-endpoints": "^2.0.2", - "@smithy/util-middleware": "^3.0.1", - "@smithy/util-retry": "^3.0.1", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -325,46 +336,47 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.598.0.tgz", - "integrity": "sha512-nOI5lqPYa+YZlrrzwAJywJSw3MKVjvu6Ge2fCqQUNYMfxFB0NAaDFnl0EPjXi+sEbtCuz/uWE77poHbqiZ+7Iw==", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.621.0.tgz", + "integrity": "sha512-xpKfikN4u0BaUYZA9FGUMkkDmfoIP0Q03+A86WjqDWhcOoqNA1DkHsE4kZ+r064ifkPUfcNuUvlkVTEoBZoFjA==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.598.0", - "@aws-sdk/middleware-host-header": "3.598.0", - "@aws-sdk/middleware-logger": "3.598.0", - "@aws-sdk/middleware-recursion-detection": "3.598.0", - "@aws-sdk/middleware-user-agent": "3.598.0", - "@aws-sdk/region-config-resolver": "3.598.0", - "@aws-sdk/types": "3.598.0", - "@aws-sdk/util-endpoints": "3.598.0", - "@aws-sdk/util-user-agent-browser": "3.598.0", - "@aws-sdk/util-user-agent-node": "3.598.0", - "@smithy/config-resolver": "^3.0.2", - "@smithy/core": "^2.2.1", - "@smithy/fetch-http-handler": "^3.0.2", - "@smithy/hash-node": "^3.0.1", - "@smithy/invalid-dependency": "^3.0.1", - "@smithy/middleware-content-length": "^3.0.1", - "@smithy/middleware-endpoint": "^3.0.2", - "@smithy/middleware-retry": "^3.0.4", - "@smithy/middleware-serde": "^3.0.1", - "@smithy/middleware-stack": "^3.0.1", - "@smithy/node-config-provider": "^3.1.1", - "@smithy/node-http-handler": "^3.0.1", - "@smithy/protocol-http": "^4.0.1", - "@smithy/smithy-client": "^3.1.2", - "@smithy/types": "^3.1.0", - "@smithy/url-parser": "^3.0.1", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.4", - "@smithy/util-defaults-mode-node": "^3.0.4", - "@smithy/util-endpoints": "^2.0.2", - "@smithy/util-middleware": "^3.0.1", - "@smithy/util-retry": "^3.0.1", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -373,98 +385,102 @@ } }, "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.600.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.600.0.tgz", - "integrity": "sha512-7+I8RWURGfzvChyNQSyj5/tKrqRbzRl7H+BnTOf/4Vsw1nFOi5ROhlhD4X/Y0QCTacxnaoNcIrqnY7uGGvVRzw==", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.621.0.tgz", + "integrity": "sha512-mMjk3mFUwV2Y68POf1BQMTF+F6qxt5tPu6daEUCNGC9Cenk3h2YXQQoS4/eSyYzuBiYk3vx49VgleRvdvkg8rg==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sts": "3.600.0", - "@aws-sdk/core": "3.598.0", - "@aws-sdk/credential-provider-node": "3.600.0", - "@aws-sdk/middleware-host-header": "3.598.0", - "@aws-sdk/middleware-logger": "3.598.0", - "@aws-sdk/middleware-recursion-detection": "3.598.0", - "@aws-sdk/middleware-user-agent": "3.598.0", - "@aws-sdk/region-config-resolver": "3.598.0", - "@aws-sdk/types": "3.598.0", - "@aws-sdk/util-endpoints": "3.598.0", - "@aws-sdk/util-user-agent-browser": "3.598.0", - "@aws-sdk/util-user-agent-node": "3.598.0", - "@smithy/config-resolver": "^3.0.2", - "@smithy/core": "^2.2.1", - "@smithy/fetch-http-handler": "^3.0.2", - "@smithy/hash-node": "^3.0.1", - "@smithy/invalid-dependency": "^3.0.1", - "@smithy/middleware-content-length": "^3.0.1", - "@smithy/middleware-endpoint": "^3.0.2", - "@smithy/middleware-retry": "^3.0.4", - "@smithy/middleware-serde": "^3.0.1", - "@smithy/middleware-stack": "^3.0.1", - "@smithy/node-config-provider": "^3.1.1", - "@smithy/node-http-handler": "^3.0.1", - "@smithy/protocol-http": "^4.0.1", - "@smithy/smithy-client": "^3.1.2", - "@smithy/types": "^3.1.0", - "@smithy/url-parser": "^3.0.1", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.4", - "@smithy/util-defaults-mode-node": "^3.0.4", - "@smithy/util-endpoints": "^2.0.2", - "@smithy/util-middleware": "^3.0.1", - "@smithy/util-retry": "^3.0.1", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.621.0" } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.600.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.600.0.tgz", - "integrity": "sha512-KQG97B7LvTtTiGmjlrG1LRAY8wUvCQzrmZVV5bjrJ/1oXAU7DITYwVbSJeX9NWg6hDuSk0VE3MFwIXS2SvfLIA==", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.621.0.tgz", + "integrity": "sha512-707uiuReSt+nAx6d0c21xLjLm2lxeKc7padxjv92CIrIocnQSlJPxSCM7r5zBhwiahJA6MNQwmTl2xznU67KgA==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.600.0", - "@aws-sdk/core": "3.598.0", - "@aws-sdk/credential-provider-node": "3.600.0", - "@aws-sdk/middleware-host-header": "3.598.0", - "@aws-sdk/middleware-logger": "3.598.0", - "@aws-sdk/middleware-recursion-detection": "3.598.0", - "@aws-sdk/middleware-user-agent": "3.598.0", - "@aws-sdk/region-config-resolver": "3.598.0", - "@aws-sdk/types": "3.598.0", - "@aws-sdk/util-endpoints": "3.598.0", - "@aws-sdk/util-user-agent-browser": "3.598.0", - "@aws-sdk/util-user-agent-node": "3.598.0", - "@smithy/config-resolver": "^3.0.2", - "@smithy/core": "^2.2.1", - "@smithy/fetch-http-handler": "^3.0.2", - "@smithy/hash-node": "^3.0.1", - "@smithy/invalid-dependency": "^3.0.1", - "@smithy/middleware-content-length": "^3.0.1", - "@smithy/middleware-endpoint": "^3.0.2", - "@smithy/middleware-retry": "^3.0.4", - "@smithy/middleware-serde": "^3.0.1", - "@smithy/middleware-stack": "^3.0.1", - "@smithy/node-config-provider": "^3.1.1", - "@smithy/node-http-handler": "^3.0.1", - "@smithy/protocol-http": "^4.0.1", - "@smithy/smithy-client": "^3.1.2", - "@smithy/types": "^3.1.0", - "@smithy/url-parser": "^3.0.1", + "@aws-sdk/client-sso-oidc": "3.621.0", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.4", - "@smithy/util-defaults-mode-node": "^3.0.4", - "@smithy/util-endpoints": "^2.0.2", - "@smithy/util-middleware": "^3.0.1", - "@smithy/util-retry": "^3.0.1", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -473,16 +489,19 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.598.0.tgz", - "integrity": "sha512-HaSjt7puO5Cc7cOlrXFCW0rtA0BM9lvzjl56x0A20Pt+0wxXGeTOZZOkXQIepbrFkV2e/HYukuT9e99vXDm59g==", - "dependencies": { - "@smithy/core": "^2.2.1", - "@smithy/protocol-http": "^4.0.1", - "@smithy/signature-v4": "^3.1.0", - "@smithy/smithy-client": "^3.1.2", - "@smithy/types": "^3.1.0", - "fast-xml-parser": "4.2.5", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.621.0.tgz", + "integrity": "sha512-CtOwWmDdEiINkGXD93iGfXjN0WmCp9l45cDWHHGa8lRgEDyhuL7bwd/pH5aSzj0j8SiQBG2k0S7DHbd5RaqvbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.3.1", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { @@ -490,14 +509,15 @@ } }, "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.600.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.600.0.tgz", - "integrity": "sha512-AIM+B06d1+71EuBrk2UR9ZZgRS3a+ARxE3oZKMZYlfqtZ3kY8w4DkhEt7OVruc6uSsMhkrcQT6nxsOxFSi4RtA==", - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.600.0", - "@aws-sdk/types": "3.598.0", - "@smithy/property-provider": "^3.1.1", - "@smithy/types": "^3.1.0", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.621.0.tgz", + "integrity": "sha512-Q+3awvTVJSqIGRjCUQflRwKPKlZ0TfmL3EQHgFLhZZrToeBapEA62+FY+T70aTKAZZZZprlvYeFPtBloNd5ziA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -505,13 +525,14 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.598.0.tgz", - "integrity": "sha512-vi1khgn7yXzLCcgSIzQrrtd2ilUM0dWodxj3PQ6BLfP0O+q1imO3hG1nq7DVyJtq7rFHs6+9N8G4mYvTkxby2w==", + "version": "3.620.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.620.1.tgz", + "integrity": "sha512-ExuILJ2qLW5ZO+rgkNRj0xiAipKT16Rk77buvPP8csR7kkCflT/gXTyzRe/uzIiETTxM7tr8xuO9MP/DQXqkfg==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/property-provider": "^3.1.1", - "@smithy/types": "^3.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -519,18 +540,19 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.598.0.tgz", - "integrity": "sha512-N7cIafi4HVlQvEgvZSo1G4T9qb/JMLGMdBsDCT5XkeJrF0aptQWzTFH0jIdZcLrMYvzPcuEyO3yCBe6cy/ba0g==", - "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/fetch-http-handler": "^3.0.2", - "@smithy/node-http-handler": "^3.0.1", - "@smithy/property-provider": "^3.1.1", - "@smithy/protocol-http": "^4.0.1", - "@smithy/smithy-client": "^3.1.2", - "@smithy/types": "^3.1.0", - "@smithy/util-stream": "^3.0.2", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.621.0.tgz", + "integrity": "sha512-/jc2tEsdkT1QQAI5Dvoci50DbSxtJrevemwFsm0B73pwCcOQZ5ZwwSdVqGsPutzYzUVx3bcXg3LRL7jLACqRIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", "tslib": "^2.6.2" }, "engines": { @@ -538,45 +560,47 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.598.0.tgz", - "integrity": "sha512-/ppcIVUbRwDIwJDoYfp90X3+AuJo2mvE52Y1t2VSrvUovYn6N4v95/vXj6LS8CNDhz2jvEJYmu+0cTMHdhI6eA==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.598.0", - "@aws-sdk/credential-provider-http": "3.598.0", - "@aws-sdk/credential-provider-process": "3.598.0", - "@aws-sdk/credential-provider-sso": "3.598.0", - "@aws-sdk/credential-provider-web-identity": "3.598.0", - "@aws-sdk/types": "3.598.0", - "@smithy/credential-provider-imds": "^3.1.1", - "@smithy/property-provider": "^3.1.1", - "@smithy/shared-ini-file-loader": "^3.1.1", - "@smithy/types": "^3.1.0", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.621.0.tgz", + "integrity": "sha512-0EWVnSc+JQn5HLnF5Xv405M8n4zfdx9gyGdpnCmAmFqEDHA8LmBdxJdpUk1Ovp/I5oPANhjojxabIW5f1uU0RA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.621.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.621.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.598.0" + "@aws-sdk/client-sts": "^3.621.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.600.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.600.0.tgz", - "integrity": "sha512-1pC7MPMYD45J7yFjA90SxpR0yaSvy+yZiq23aXhAPZLYgJBAxHLu0s0mDCk/piWGPh8+UGur5K0bVdx4B1D5hw==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.598.0", - "@aws-sdk/credential-provider-http": "3.598.0", - "@aws-sdk/credential-provider-ini": "3.598.0", - "@aws-sdk/credential-provider-process": "3.598.0", - "@aws-sdk/credential-provider-sso": "3.598.0", - "@aws-sdk/credential-provider-web-identity": "3.598.0", - "@aws-sdk/types": "3.598.0", - "@smithy/credential-provider-imds": "^3.1.1", - "@smithy/property-provider": "^3.1.1", - "@smithy/shared-ini-file-loader": "^3.1.1", - "@smithy/types": "^3.1.0", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.621.0.tgz", + "integrity": "sha512-4JqpccUgz5Snanpt2+53hbOBbJQrSFq7E1sAAbgY6BKVQUsW5qyXqnjvSF32kDeKa5JpBl3bBWLZl04IadcPHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.621.0", + "@aws-sdk/credential-provider-ini": "3.621.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.621.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -584,14 +608,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.598.0.tgz", - "integrity": "sha512-rM707XbLW8huMk722AgjVyxu2tMZee++fNA8TJVNgs1Ma02Wx6bBrfIvlyK0rCcIRb0WdQYP6fe3Xhiu4e8IBA==", - "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/property-provider": "^3.1.1", - "@smithy/shared-ini-file-loader": "^3.1.1", - "@smithy/types": "^3.1.0", + "version": "3.620.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.620.1.tgz", + "integrity": "sha512-hWqFMidqLAkaV9G460+1at6qa9vySbjQKKc04p59OT7lZ5cO5VH5S4aI05e+m4j364MBROjjk2ugNvfNf/8ILg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -599,16 +624,17 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.598.0.tgz", - "integrity": "sha512-5InwUmrAuqQdOOgxTccRayMMkSmekdLk6s+az9tmikq0QFAHUCtofI+/fllMXSR9iL6JbGYi1940+EUmS4pHJA==", - "dependencies": { - "@aws-sdk/client-sso": "3.598.0", - "@aws-sdk/token-providers": "3.598.0", - "@aws-sdk/types": "3.598.0", - "@smithy/property-provider": "^3.1.1", - "@smithy/shared-ini-file-loader": "^3.1.1", - "@smithy/types": "^3.1.0", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.621.0.tgz", + "integrity": "sha512-Kza0jcFeA/GEL6xJlzR2KFf1PfZKMFnxfGzJzl5yN7EjoGdMijl34KaRyVnfRjnCWcsUpBWKNIDk9WZVMY9yiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.621.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -616,42 +642,44 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.598.0.tgz", - "integrity": "sha512-GV5GdiMbz5Tz9JO4NJtRoFXjW0GPEujA0j+5J/B723rTN+REHthJu48HdBKouHGhdzkDWkkh1bu52V02Wprw8w==", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.621.0.tgz", + "integrity": "sha512-w7ASSyfNvcx7+bYGep3VBgC3K6vEdLmlpjT7nSIHxxQf+WSdvy+HynwJosrpZax0sK5q0D1Jpn/5q+r5lwwW6w==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/property-provider": "^3.1.1", - "@smithy/types": "^3.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.598.0" + "@aws-sdk/client-sts": "^3.621.0" } }, "node_modules/@aws-sdk/credential-providers": { - "version": "3.600.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.600.0.tgz", - "integrity": "sha512-cC9uqmX0rgx1efiJGqeR+i0EXr8RQ5SAzH7M45WNBZpYiLEe6reWgIYJY9hmOxuaoMdWSi8kekuN3IjTIORRjw==", - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.600.0", - "@aws-sdk/client-sso": "3.598.0", - "@aws-sdk/client-sts": "3.600.0", - "@aws-sdk/credential-provider-cognito-identity": "3.600.0", - "@aws-sdk/credential-provider-env": "3.598.0", - "@aws-sdk/credential-provider-http": "3.598.0", - "@aws-sdk/credential-provider-ini": "3.598.0", - "@aws-sdk/credential-provider-node": "3.600.0", - "@aws-sdk/credential-provider-process": "3.598.0", - "@aws-sdk/credential-provider-sso": "3.598.0", - "@aws-sdk/credential-provider-web-identity": "3.598.0", - "@aws-sdk/types": "3.598.0", - "@smithy/credential-provider-imds": "^3.1.1", - "@smithy/property-provider": "^3.1.1", - "@smithy/types": "^3.1.0", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.621.0.tgz", + "integrity": "sha512-FQbC7I8ae/72ZekLBa45jWJ+Q3d+YPhc3bW/rCks6RrldM6RgLTGr8pTOPCxHl828ky10RjkBiBmVU818rliyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.621.0", + "@aws-sdk/client-sso": "3.621.0", + "@aws-sdk/client-sts": "3.621.0", + "@aws-sdk/credential-provider-cognito-identity": "3.621.0", + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.621.0", + "@aws-sdk/credential-provider-ini": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.621.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -659,13 +687,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.598.0.tgz", - "integrity": "sha512-WiaG059YBQwQraNejLIi0gMNkX7dfPZ8hDIhvMr5aVPRbaHH8AYF3iNSsXYCHvA2Cfa1O9haYXsuMF9flXnCmA==", + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.620.0.tgz", + "integrity": "sha512-VMtPEZwqYrII/oUkffYsNWY9PZ9xpNJpMgmyU0rlDQ25O1c0Hk3fJmZRe6pEkAJ0omD7kLrqGl1DUjQVxpd/Rg==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/protocol-http": "^4.0.1", - "@smithy/types": "^3.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -673,12 +702,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.598.0.tgz", - "integrity": "sha512-bxBjf/VYiu3zfu8SYM2S9dQQc3tz5uBAOcPz/Bt8DyyK3GgOpjhschH/2XuUErsoUO1gDJqZSdGOmuHGZQn00Q==", + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.609.0.tgz", + "integrity": "sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/types": "^3.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -686,13 +716,14 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.598.0.tgz", - "integrity": "sha512-vjT9BeFY9FeN0f8hm2l6F53tI0N5bUq6RcDkQXKNabXBnQxKptJRad6oP2X5y3FoVfBLOuDkQgiC2940GIPxtQ==", + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.620.0.tgz", + "integrity": "sha512-nh91S7aGK3e/o1ck64sA/CyoFw+gAYj2BDOnoNa6ouyCrVJED96ZXWbhye/fz9SgmNUZR2g7GdVpiLpMKZoI5w==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/protocol-http": "^4.0.1", - "@smithy/types": "^3.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -700,14 +731,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.598.0.tgz", - "integrity": "sha512-4tjESlHG5B5MdjUaLK7tQs/miUtHbb6deauQx8ryqSBYOhfHVgb1ZnzvQR0bTrhpqUg0WlybSkDaZAICf9xctg==", - "dependencies": { - "@aws-sdk/types": "3.598.0", - "@aws-sdk/util-endpoints": "3.598.0", - "@smithy/protocol-http": "^4.0.1", - "@smithy/types": "^3.1.0", + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.620.0.tgz", + "integrity": "sha512-bvS6etn+KsuL32ubY5D3xNof1qkenpbJXf/ugGXbg0n98DvDFQ/F+SMLxHgbnER5dsKYchNnhmtI6/FC3HFu/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -715,15 +747,16 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.598.0.tgz", - "integrity": "sha512-oYXhmTokSav4ytmWleCr3rs/1nyvZW/S0tdi6X7u+dLNL5Jee+uMxWGzgOrWK6wrQOzucLVjS4E/wA11Kv2GTw==", + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.614.0.tgz", + "integrity": "sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/node-config-provider": "^3.1.1", - "@smithy/types": "^3.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.1", + "@smithy/util-middleware": "^3.0.3", "tslib": "^2.6.2" }, "engines": { @@ -731,29 +764,31 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.598.0.tgz", - "integrity": "sha512-TKY1EVdHVBnZqpyxyTHdpZpa1tUpb6nxVeRNn1zWG8QB5MvH4ALLd/jR+gtmWDNQbIG4cVuBOZFVL8hIYicKTA==", - "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/property-provider": "^3.1.1", - "@smithy/shared-ini-file-loader": "^3.1.1", - "@smithy/types": "^3.1.0", + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.614.0.tgz", + "integrity": "sha512-okItqyY6L9IHdxqs+Z116y5/nda7rHxLvROxtAJdLavWTYDydxrZstImNgGWTeVdmc0xX2gJCI77UYUTQWnhRw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.598.0" + "@aws-sdk/client-sso-oidc": "^3.614.0" } }, "node_modules/@aws-sdk/types": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.598.0.tgz", - "integrity": "sha512-742uRl6z7u0LFmZwDrFP6r1wlZcgVPw+/TilluDJmCAR8BgRw3IR+743kUXKBGd8QZDRW2n6v/PYsi/AWCDDMQ==", + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", + "integrity": "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.1.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -761,13 +796,14 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.598.0.tgz", - "integrity": "sha512-Qo9UoiVVZxcOEdiOMZg3xb1mzkTxrhd4qSlg5QQrfWPJVx/QOg+Iy0NtGxPtHtVZNHZxohYwDwV/tfsnDSE2gQ==", + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.614.0.tgz", + "integrity": "sha512-wK2cdrXHH4oz4IomV/yrGkftU9A+ITB6nFL+rxxyO78is2ifHJpFdV4aqk4LSkXYPi6CXWNru/Dqc7yiKXgJPw==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/types": "^3.1.0", - "@smithy/util-endpoints": "^2.0.2", + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.5", "tslib": "^2.6.2" }, "engines": { @@ -778,6 +814,7 @@ "version": "3.568.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz", "integrity": "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -786,24 +823,26 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.598.0.tgz", - "integrity": "sha512-36Sxo6F+ykElaL1mWzWjlg+1epMpSe8obwhCN1yGE7Js9ywy5U6k6l+A3q3YM9YRbm740sNxncbwLklMvuhTKw==", + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.609.0.tgz", + "integrity": "sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/types": "^3.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.598.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.598.0.tgz", - "integrity": "sha512-oyWGcOlfTdzkC6SVplyr0AGh54IMrDxbhg5RxJ5P+V4BKfcDoDcZV9xenUk9NsOi9MuUjxMumb9UJGkDhM1m0A==", + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.614.0.tgz", + "integrity": "sha512-15ElZT88peoHnq5TEoEtZwoXTXRxNrk60TZNdpl/TUBJ5oNJ9Dqb5Z4ryb8ofN6nm9aFf59GVAerFDz8iUoHBA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.598.0", - "@smithy/node-config-provider": "^3.1.1", - "@smithy/types": "^3.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -1479,6 +1518,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8" }, @@ -1510,6 +1550,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -1524,6 +1565,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -1799,6 +1841,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "license": "MIT", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", @@ -1819,6 +1862,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -1827,6 +1871,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@electron/remote/-/remote-2.1.2.tgz", "integrity": "sha512-EPwNx+nhdrTBxyCqXt/pftoQg/ybtWDW3DUWHafejvnB1ZGGfMpv6e15D8KeempocjXe78T7WreyGGb3mlZxdA==", + "license": "MIT", "peerDependencies": { "electron": ">= 13.0.0" } @@ -3504,25 +3549,39 @@ } }, "node_modules/@mongodb-js/compass-logging": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-logging/-/compass-logging-1.3.1.tgz", - "integrity": "sha512-EguV4j8J6yV0lffM6K87ie2l189Wy3frHJ3wUHds8THBFkxH+wfbz4QPZav+AMd+BmsA2gQwD7RCO+7CvBc1qQ==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-logging/-/compass-logging-1.4.4.tgz", + "integrity": "sha512-bZWRep/WBPG2Ao7PGgCmRq9L7R2+njh+RCxeWxHd7YXOjEHQYdfxOzFks7oogLWF0O6dvO856nINFMTj5xbqZw==", + "license": "SSPL", "dependencies": { "debug": "^4.3.4", - "hadron-app-registry": "^9.1.12", - "hadron-ipc": "^3.2.16", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "is-electron-renderer": "^2.0.1", "mongodb-log-writer": "^1.4.2", "react": "^17.0.2" } }, + "node_modules/@mongodb-js/compass-user-data": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-user-data/-/compass-user-data-0.3.4.tgz", + "integrity": "sha512-nwdkVNVPlf7bD591ShMOW+qNHySp/v88z0ExKVS6NIsv30YTuMogZuSeIt2i3d+0ThEKVeR3Urp75diSWaysNg==", + "license": "SSPL", + "dependencies": { + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-utils": "^0.6.10", + "write-file-atomic": "^5.0.1", + "zod": "^3.22.3" + } + }, "node_modules/@mongodb-js/compass-utils": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-utils/-/compass-utils-0.6.5.tgz", - "integrity": "sha512-0Q6cUJo/58Fh6rnrkBhmr0o9X2Hkp8ruv2YSC2eRE1qo5aTX8PoS5nZ2f6EO9HYL79XtAgLgmYZQqZm5P2AlGA==", + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-utils/-/compass-utils-0.6.10.tgz", + "integrity": "sha512-zokjRzVelu7xsLXnm26kQB2Vtbt3pJyInNQxrZPW9rqaOZTAZIWh/0Kr9bb3A4WdCSSiu3P4nty5OHMsSXGF4A==", + "license": "SSPL", "dependencies": { "@electron/remote": "^2.1.2", - "electron": "^29.4.2" + "electron": "^30.4.0" } }, "node_modules/@mongodb-js/connection-form": { @@ -3638,83 +3697,47 @@ } }, "node_modules/@mongodb-js/devtools-proxy-support": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.2.tgz", - "integrity": "sha512-qMSe/5XVEK3xXtMhtv+InIRuanH5nDdDo8yD3gFvsw5pRhI9qM5m06imfgx9X1woAFdntIUNy72lGNi2glbOaA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", + "license": "Apache-2.0", "dependencies": { "@mongodb-js/socksv5": "^0.0.10", "agent-base": "^7.1.1", + "debug": "^4.3.6", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "lru-cache": "^11.0.0", "node-fetch": "^3.3.2", - "pac-proxy-agent": "7.0.2", + "pac-proxy-agent": "^7.0.2", "socks-proxy-agent": "^8.0.4", "ssh2": "^1.15.0", "system-ca": "^2.0.0" } }, - "node_modules/@mongodb-js/devtools-proxy-support/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@mongodb-js/devtools-proxy-support/node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", "engines": { "node": ">= 12" } }, - "node_modules/@mongodb-js/devtools-proxy-support/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@mongodb-js/devtools-proxy-support/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@mongodb-js/devtools-proxy-support/node_modules/lru-cache": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "license": "ISC", "engines": { "node": "20 || >=22" } }, - "node_modules/@mongodb-js/devtools-proxy-support/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "optional": true - }, "node_modules/@mongodb-js/devtools-proxy-support/node_modules/node-fetch": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -3728,29 +3751,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/@mongodb-js/devtools-proxy-support/node_modules/system-ca": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/system-ca/-/system-ca-2.0.1.tgz", - "integrity": "sha512-9ZDV9yl8ph6Op67wDGPr4LykX86usE9x3le+XZSHfVMiiVJ5IRgmCWjLgxyz35ju9H3GDIJJZm4ogAeIfN5cQQ==", - "optionalDependencies": { - "macos-export-certificate-and-key": "^1.2.0", - "win-export-certificate-and-key": "^2.1.0" - } - }, - "node_modules/@mongodb-js/devtools-proxy-support/node_modules/win-export-certificate-and-key": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/win-export-certificate-and-key/-/win-export-certificate-and-key-2.1.0.tgz", - "integrity": "sha512-WeMLa/2uNZcS/HWGKU2G1Gzeh3vHpV/UFvwLhJLKxPHYFAbubxxVcJbqmPXaqySWK1Ymymh16zKK5WYIJ3zgzA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "win32" - ], - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^3.1.0" - } - }, "node_modules/@mongodb-js/mongodb-constants": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.10.2.tgz", @@ -3773,9 +3773,10 @@ } }, "node_modules/@mongodb-js/oidc-http-server-pages": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.1.1.tgz", - "integrity": "sha512-zYkANxb4HVDN4zGuU2ud4fL+XpqRxaydCwVZyU9Mh5EhqbrCL5mGH7t3BaGnfdQUw+HVtwP3dmasc+5Hjewk+g==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.1.2.tgz", + "integrity": "sha512-YcRlJ/HkKMahWvaPFTN/al5MGGX6CKQTsBHFTpNduxmeHd40jO8Cj5W7dfrqJAoit5E/G+GOwMaEfWdNrdlihg==", + "license": "Apache-2.0" }, "node_modules/@mongodb-js/oidc-mock-provider": { "version": "0.9.1", @@ -3883,6 +3884,18 @@ "mongodb-sbom-tools": "bin/mongodb-sbom-tools.js" } }, + "node_modules/@mongodb-js/shell-bson-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/shell-bson-parser/-/shell-bson-parser-1.1.1.tgz", + "integrity": "sha512-MqFWxo3D1S4lpxPR+IOi0CahRQiwrCH0OiCYBA16Y5VojtWENs2Kq8hqxor2V3ZJhTHqpfJFs2858DAcAYdSeA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.1.0" + }, + "peerDependencies": { + "bson": "^4.6.3 || ^5 || ^6" + } + }, "node_modules/@mongodb-js/signing-utils": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@mongodb-js/signing-utils/-/signing-utils-0.3.5.tgz", @@ -3898,6 +3911,7 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/@mongodb-js/socksv5/-/socksv5-0.0.10.tgz", "integrity": "sha512-JDz2fLKsjMiSNUxKrCpGptsgu7DzsXfu4gnUQ3RhUaBS1d4YbLrt6HejpckAiHIAa+niBpZAeiUsoop0IihWsw==", + "license": "MIT", "dependencies": { "ip-address": "^9.0.5" }, @@ -3905,23 +3919,14 @@ "node": ">=0.10.0" } }, - "node_modules/@mongodb-js/ssh-tunnel": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/ssh-tunnel/-/ssh-tunnel-2.2.1.tgz", - "integrity": "sha512-uNLYJzBVi1+iJfrENlUenBTdJp4d0E3dGEw5z5ngK3RDROz64/I3DgP+zjWIU5KciygFK69GNUPgtDfzSJjzAw==", - "dependencies": { - "@mongodb-js/compass-logging": "^1.3.1", - "socksv5": "0.0.6", - "ssh2": "^1.12.0" - } - }, "node_modules/@mongosh/arg-parser": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/arg-parser/-/arg-parser-2.2.15.tgz", - "integrity": "sha512-KGYiw5bl8cv6wDSrTCDF1B2PjddPhiD5BkulXvgpkfuD5bB2zTJSgpx+EGGcD60OSDRnMdk0tu9AY8uIExtNvA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/arg-parser/-/arg-parser-2.3.0.tgz", + "integrity": "sha512-4vWpODQfL/4WS+7DTaeXfGWtkHT7N5CZaTbaasi3pjPu49WC3sLko9civwRJWN3ygoyxADDZrfhRCDELvXg2eg==", + "license": "Apache-2.0", "dependencies": { - "@mongosh/errors": "2.2.15", - "@mongosh/i18n": "2.2.15", + "@mongosh/errors": "2.3.0", + "@mongosh/i18n": "2.3.0", "mongodb-connection-string-url": "^3.0.1" }, "engines": { @@ -3929,9 +3934,10 @@ } }, "node_modules/@mongosh/async-rewriter2": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/async-rewriter2/-/async-rewriter2-2.2.15.tgz", - "integrity": "sha512-y7LyjulLYe0QodRa4YIpvpHt23VQWrFGx4C5AD3IVVFhgNd0yxg2bWLIMaFsM7wwgbGJU3BxnVecAnHOgiuRHg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/async-rewriter2/-/async-rewriter2-2.3.0.tgz", + "integrity": "sha512-eYCxW76EeYDWZOsMHKIZAVXq0ePmiPOiy4SQ8sryDYluAzZR1e/2ZWMwwMG5tss1lkpG3q+nKTWsphFGCMUsLQ==", + "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.22.8", "@babel/plugin-transform-destructuring": "^7.22.5", @@ -3947,12 +3953,13 @@ } }, "node_modules/@mongosh/autocomplete": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/autocomplete/-/autocomplete-2.2.15.tgz", - "integrity": "sha512-R1rZVWLNmlOsOVGoHCdAxB0mx7J1A4ElPvzRBWcPW+PSEzlTT/9j0AT87exK/jjUE8ZnkzUw/soh4tqFQIjwAA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/autocomplete/-/autocomplete-2.3.0.tgz", + "integrity": "sha512-64BZB+ruZ5leJrkA0QTKJpzYpezbpwdXD/e26D0H+zlKsL3/cX2+ZZVsZbBYTPbqickXPd52sYvz5H2bsiqVnA==", + "license": "Apache-2.0", "dependencies": { "@mongodb-js/mongodb-constants": "^0.10.1", - "@mongosh/shell-api": "2.2.15", + "@mongosh/shell-api": "2.3.0", "semver": "^7.5.4" }, "engines": { @@ -3960,44 +3967,48 @@ } }, "node_modules/@mongosh/browser-runtime-core": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-core/-/browser-runtime-core-2.2.15.tgz", - "integrity": "sha512-jBy6GizoPEvwDJCl53YDQY5Lv1F4ADL0DEqaFvKk0Ltav8EkvCcsmZTY6Kf9MmgVJGXTawWSxKoj5KLj2QUr4g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-core/-/browser-runtime-core-2.3.0.tgz", + "integrity": "sha512-8BMqJTBZbCcMeBzGSf2DpP0PUU4+Gh9JHgieYVj6IUvqx0XL9zRNbSHBbfpjnIt+Baym8L2Ye2+GwILhARZmYg==", + "license": "Apache-2.0", "dependencies": { - "@mongosh/autocomplete": "2.2.15", - "@mongosh/service-provider-core": "2.2.15", - "@mongosh/shell-api": "2.2.15", - "@mongosh/shell-evaluator": "2.2.15" + "@mongosh/autocomplete": "2.3.0", + "@mongosh/service-provider-core": "2.3.0", + "@mongosh/shell-api": "2.3.0", + "@mongosh/shell-evaluator": "2.3.0" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/browser-runtime-electron": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-electron/-/browser-runtime-electron-2.2.15.tgz", - "integrity": "sha512-bMxxjn9F6nxvu/qklOUc9jO9nuK24b5qzMZblHgJilicmFVtUPofZ6B5bEaxYoQHEHyB7RITs/MZmXpV6mDehg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/browser-runtime-electron/-/browser-runtime-electron-2.3.0.tgz", + "integrity": "sha512-UayVEYZoU3ZrGc51QC8talqMDw+dZDj/BOEiMU/UkEUwlSYTQUmVI/A3Urxww+EQ7a+QqL51a/F5KNK8E61WPg==", + "license": "Apache-2.0", "dependencies": { - "@mongosh/browser-runtime-core": "2.2.15", - "@mongosh/service-provider-core": "2.2.15", - "@mongosh/types": "2.2.15" + "@mongosh/browser-runtime-core": "2.3.0", + "@mongosh/service-provider-core": "2.3.0", + "@mongosh/types": "2.3.0" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/errors": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/errors/-/errors-2.2.15.tgz", - "integrity": "sha512-RHCRv3Fg/xWS5XV4hOyh6KDBrn2kld+J5PVtXfsuke73jfQTLlR2PGMzSEpPWiayRLgLExq56qdXGOtNecmhuA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-ul6OGLrCxWZ0C+g+WUwDZDXpZTATkri9yt3W/HP050h35jW37qWBeOi4FG9kwTwakgoaYiei87SwPfvN/M8oqg==", + "license": "Apache-2.0", "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/history": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/history/-/history-2.2.15.tgz", - "integrity": "sha512-GV1i3RmG38+OUxBnqTeAlcPezkJ4fH3bBs4bwvLEV7iXMcVNzNoJBMyDa7gO6er45w38Kczx9kVDIOYdutt2Yg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/history/-/history-2.3.0.tgz", + "integrity": "sha512-4ZBQxvaX1iazXImLzQsDR1hg7HY8FNtHsLFJYsMaTzN+995wAvqGUFYIFiETzArQ7/Vi+FVGx40kzIjYa5nzmA==", + "license": "Apache-2.0", "dependencies": { "mongodb-connection-string-url": "^3.0.1", "mongodb-redact": "^1.1.2" @@ -4007,44 +4018,91 @@ } }, "node_modules/@mongosh/i18n": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/i18n/-/i18n-2.2.15.tgz", - "integrity": "sha512-7pjQbvJbtaglZKj86/2GRQnXLRekmpTPIVR2M58kAVXaNGqGrfCpe6mkBEkIwdjk6UHQIvkwMSzUIbFGm7nFvA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/i18n/-/i18n-2.3.0.tgz", + "integrity": "sha512-JnIY7dxPOr3WFZ0YFfB/i53flYr0tIUiM3i9xo2DXWyeaQiuHxQxYwmCIDlVkGhvLVVlSXY2U0ptA0AW0YNztg==", + "license": "Apache-2.0", "dependencies": { - "@mongosh/errors": "2.2.15" + "@mongosh/errors": "2.3.0" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/service-provider-core": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/service-provider-core/-/service-provider-core-2.2.15.tgz", - "integrity": "sha512-Pk+Sxxf0rE7KacEMZvhGjr15cWkV+lcbI8cv5Hf7Taxj8kLXfbKM45WBIgGtMDTh/fbmbT15qI7StG5sCO8CCg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/service-provider-core/-/service-provider-core-2.3.0.tgz", + "integrity": "sha512-0DQUKVhfpwJRiFvTc4gjG6dxNDZUWTAGnl9ihfIL5ShxNKXluo+9VcYEL1wxLTbF1xZaMeCDzH6rIM02G5PM3w==", + "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-providers": "^3.525.0", - "@mongosh/errors": "2.2.15", + "@mongosh/errors": "2.3.0", "bson": "^6.7.0", "mongodb": "^6.8.0", - "mongodb-build-info": "^1.7.2" + "mongodb-build-info": "^1.7.2", + "mongodb-connection-string-url": "^3.0.1" }, "engines": { "node": ">=14.15.1" }, "optionalDependencies": { - "mongodb-client-encryption": "^6.0.0" + "mongodb-client-encryption": "^6.1.0" + } + }, + "node_modules/@mongosh/service-provider-core/node_modules/mongodb-client-encryption": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.1.0.tgz", + "integrity": "sha512-Y3Hakre82nXD/pNDUzBjxfgwWSj5E1ar9ZLkqyXDfvirv4huHMbg8Q2qVO/TXlNJuf1B2bzrEDXsTqHKQSQLtw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/@mongosh/service-provider-core/node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/@mongosh/service-provider-server": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/service-provider-server/-/service-provider-server-2.2.15.tgz", - "integrity": "sha512-TtQVsMlQhZqRQ6Aj9tUcSvNLL6tthkzx3m/FhBxyT/7rxxuK56w0IUVg31Cqq/gz9xUfkP3JiKounIgYqRCbXQ==", - "dependencies": { - "@mongodb-js/devtools-connect": "^3.0.5", - "@mongodb-js/oidc-plugin": "^1.0.2", - "@mongosh/errors": "2.2.15", - "@mongosh/service-provider-core": "2.2.15", - "@mongosh/types": "2.2.15", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/service-provider-server/-/service-provider-server-2.3.0.tgz", + "integrity": "sha512-fzdhnUM9LPo1PWJ5QUphATnytRgzH9BhcyNhtL8yEHDP8lwwRplQrwk801u1imgI8e4oYggauMzNZQRgxQWVog==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/devtools-connect": "^3.2.5", + "@mongodb-js/oidc-plugin": "^1.1.1", + "@mongosh/errors": "2.3.0", + "@mongosh/service-provider-core": "2.3.0", + "@mongosh/types": "2.3.0", "aws4": "^1.12.0", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", @@ -4055,15 +4113,16 @@ }, "optionalDependencies": { "kerberos": "^2.1.0", - "mongodb-client-encryption": "^6.0.0" + "mongodb-client-encryption": "^6.1.0" } }, "node_modules/@mongosh/service-provider-server/node_modules/@mongodb-js/devtools-connect": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.2.tgz", - "integrity": "sha512-0d/9hiNnVxFjgu0HtbUSMOem/hMtpj7aKj/QN3UsABGJ8jBxMXzE90jNP6JOJ+Nt5dmlCX2iTMvtJiBIWOtCZA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.6.tgz", + "integrity": "sha512-E5jvGDHZ13fnDkuIytnINIS2/2BR0aiC0rfXLKeOO6ongJfL8F5ACEz5dbCR+e6eJ4JCeh1Tb49CfjZK9iGhWQ==", + "license": "Apache-2.0", "dependencies": { - "@mongodb-js/devtools-proxy-support": "^0.3.2", + "@mongodb-js/devtools-proxy-support": "^0.3.6", "@mongodb-js/oidc-http-server-pages": "1.1.2", "lodash.merge": "^4.6.2", "mongodb-connection-string-url": "^3.0.0", @@ -4081,15 +4140,11 @@ "mongodb-log-writer": "^1.4.2" } }, - "node_modules/@mongosh/service-provider-server/node_modules/@mongodb-js/oidc-http-server-pages": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.1.2.tgz", - "integrity": "sha512-YcRlJ/HkKMahWvaPFTN/al5MGGX6CKQTsBHFTpNduxmeHd40jO8Cj5W7dfrqJAoit5E/G+GOwMaEfWdNrdlihg==" - }, "node_modules/@mongosh/service-provider-server/node_modules/@mongodb-js/oidc-plugin": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", + "license": "Apache-2.0", "dependencies": { "express": "^4.18.2", "open": "^9.1.0", @@ -4099,16 +4154,60 @@ "node": ">= 16.20.1" } }, + "node_modules/@mongosh/service-provider-server/node_modules/mongodb-client-encryption": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.1.0.tgz", + "integrity": "sha512-Y3Hakre82nXD/pNDUzBjxfgwWSj5E1ar9ZLkqyXDfvirv4huHMbg8Q2qVO/TXlNJuf1B2bzrEDXsTqHKQSQLtw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/@mongosh/service-provider-server/node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@mongosh/shell-api": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/shell-api/-/shell-api-2.2.15.tgz", - "integrity": "sha512-HkJhDKWHRRqa7fznsRVp/ivolM7RKeCyTuJXMVFym3qt4wlC63Tc3IQjm8HYORlFGRz04AOOwCgzkIp8ddPXkg==", - "dependencies": { - "@mongosh/arg-parser": "2.2.15", - "@mongosh/errors": "2.2.15", - "@mongosh/history": "2.2.15", - "@mongosh/i18n": "2.2.15", - "@mongosh/service-provider-core": "2.2.15", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/shell-api/-/shell-api-2.3.0.tgz", + "integrity": "sha512-Xzyg1T20YVywE1lXwhxR2muDUA4F2ClYaeAPc4bjQabNT9fWbuJXqbGBSwy36sv0rpqTiZqWJoFQcAgMfAt0PQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongosh/arg-parser": "2.3.0", + "@mongosh/errors": "2.3.0", + "@mongosh/history": "2.3.0", + "@mongosh/i18n": "2.3.0", + "@mongosh/service-provider-core": "2.3.0", "mongodb-redact": "^1.1.2" }, "engines": { @@ -4116,35 +4215,38 @@ } }, "node_modules/@mongosh/shell-evaluator": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/shell-evaluator/-/shell-evaluator-2.2.15.tgz", - "integrity": "sha512-Km/rThnbklPiYfNd/K1qFUNXICMRaYVq1pOWWSYbrT7a97KcFHIoD2OgUUudksuva4zc24CfeP5GSWRtYpbq+w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/shell-evaluator/-/shell-evaluator-2.3.0.tgz", + "integrity": "sha512-TvWFrFkZ+LORZbAWu6tZHeJs+OQwKPli1sEHAVKjhhjiNo4v/kV/3990Dcqre2IZNBpybaZweTChf3b7Y5Lijw==", + "license": "Apache-2.0", "dependencies": { - "@mongosh/async-rewriter2": "2.2.15", - "@mongosh/history": "2.2.15", - "@mongosh/shell-api": "2.2.15" + "@mongosh/async-rewriter2": "2.3.0", + "@mongosh/history": "2.3.0", + "@mongosh/shell-api": "2.3.0" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/types": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@mongosh/types/-/types-2.2.15.tgz", - "integrity": "sha512-HkhZkjrkK9w+QHd2kPl7mspZUOpCUmgEvvHLMHmhpaYksLcxm2H4/H+s5F1Kj3EpuC9yyOHuvfC3ZMhDOgF0tg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mongosh/types/-/types-2.3.0.tgz", + "integrity": "sha512-55V8S7Awj9fGi3cAf0/9svGagErnChrK4hOY2Z4C4y06vN5t/EUO15lyZYHJFUldnydfZzHODkPjjyWV53a1XQ==", + "license": "Apache-2.0", "dependencies": { - "@mongodb-js/devtools-connect": "^3.0.5" + "@mongodb-js/devtools-connect": "^3.2.5" }, "engines": { "node": ">=14.15.1" } }, "node_modules/@mongosh/types/node_modules/@mongodb-js/devtools-connect": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.2.tgz", - "integrity": "sha512-0d/9hiNnVxFjgu0HtbUSMOem/hMtpj7aKj/QN3UsABGJ8jBxMXzE90jNP6JOJ+Nt5dmlCX2iTMvtJiBIWOtCZA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.6.tgz", + "integrity": "sha512-E5jvGDHZ13fnDkuIytnINIS2/2BR0aiC0rfXLKeOO6ongJfL8F5ACEz5dbCR+e6eJ4JCeh1Tb49CfjZK9iGhWQ==", + "license": "Apache-2.0", "dependencies": { - "@mongodb-js/devtools-proxy-support": "^0.3.2", + "@mongodb-js/devtools-proxy-support": "^0.3.6", "@mongodb-js/oidc-http-server-pages": "1.1.2", "lodash.merge": "^4.6.2", "mongodb-connection-string-url": "^3.0.0", @@ -4162,15 +4264,11 @@ "mongodb-log-writer": "^1.4.2" } }, - "node_modules/@mongosh/types/node_modules/@mongodb-js/oidc-http-server-pages": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-http-server-pages/-/oidc-http-server-pages-1.1.2.tgz", - "integrity": "sha512-YcRlJ/HkKMahWvaPFTN/al5MGGX6CKQTsBHFTpNduxmeHd40jO8Cj5W7dfrqJAoit5E/G+GOwMaEfWdNrdlihg==" - }, "node_modules/@mongosh/types/node_modules/@mongodb-js/oidc-plugin": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", + "license": "Apache-2.0", "peer": true, "dependencies": { "express": "^4.18.2", @@ -4654,11 +4752,12 @@ "dev": true }, "node_modules/@smithy/abort-controller": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.0.tgz", - "integrity": "sha512-XOm4LkuC0PsK1sf2bBJLIlskn5ghmVxiEBVlo/jg0R8hxASBKYYgOoJEhKWgOr4vWGkN+5rC+oyBAqHYtxjnwQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", + "integrity": "sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4666,14 +4765,15 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.3.tgz", - "integrity": "sha512-4wHqCMkdfVDP4qmr4fVPYOFOH+vKhOv3X4e6KEU9wIC8xXUQ24tnF4CW+sddGDX1zU86GGyQ7A+rg2xmUD6jpQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.5.tgz", + "integrity": "sha512-SkW5LxfkSI1bUC74OtfBbdz+grQXYiPYolyu8VfpLIjEoN/sHVBlLeGXMQ1vX4ejkgfv6sxVbQJ32yF2cl1veA==", + "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.2", - "@smithy/types": "^3.2.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.2", + "@smithy/util-middleware": "^3.0.3", "tslib": "^2.6.2" }, "engines": { @@ -4681,17 +4781,20 @@ } }, "node_modules/@smithy/core": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.2.3.tgz", - "integrity": "sha512-SpyLOL2vgE6sUYM6nQfu82OirCPkCDKctyG3aMgjMlDPTJpUlmlNH0ttu9ZWwzEjrzzr8uABmPjJTRI7gk1HFQ==", - "dependencies": { - "@smithy/middleware-endpoint": "^3.0.3", - "@smithy/middleware-retry": "^3.0.6", - "@smithy/middleware-serde": "^3.0.2", - "@smithy/protocol-http": "^4.0.2", - "@smithy/smithy-client": "^3.1.4", - "@smithy/types": "^3.2.0", - "@smithy/util-middleware": "^3.0.2", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.4.0.tgz", + "integrity": "sha512-cHXq+FneIF/KJbt4q4pjN186+Jf4ZB0ZOqEaZMBhT79srEyGDDBV31NqBRBjazz8ppQ1bJbDJMY9ba5wKFV36w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { @@ -4699,14 +4802,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.1.2.tgz", - "integrity": "sha512-gqVmUaNoeqyrOAjgZg+rTmFLsphh/vS59LCMdFfVpthVS0jbfBzvBmEPktBd+y9ME4DYMGHFAMSYJDK8q0noOQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.0.tgz", + "integrity": "sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA==", + "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.2", - "@smithy/property-provider": "^3.1.2", - "@smithy/types": "^3.2.0", - "@smithy/url-parser": "^3.0.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "tslib": "^2.6.2" }, "engines": { @@ -4714,23 +4818,25 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.1.0.tgz", - "integrity": "sha512-s7oQjEOUH9TYjctpITtWF4qxOdg7pBrP9eigEQ8SBsxF3dRFV0S28pGMllC83DUr7ECmErhO/BUwnULfoNhKgQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.4.tgz", + "integrity": "sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.0.2", - "@smithy/querystring-builder": "^3.0.2", - "@smithy/types": "^3.2.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" } }, "node_modules/@smithy/hash-node": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.2.tgz", - "integrity": "sha512-43uGA6o6QJQdXwAogybdTDHDd3SCdKyoiHIHb8PpdE2rKmVicjG9b1UgVwdgO8QPytmVqHFaUw27M3LZKwu8Yg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.3.tgz", + "integrity": "sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -4740,11 +4846,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.2.tgz", - "integrity": "sha512-+BAY3fMhomtq470tswXyrdVBSUhiLuhBVT+rOmpbz5e04YX+s1dX4NxTLzZGwBjCpeWZNtTxP8zbIvvFk81gUg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.3.tgz", + "integrity": "sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, @@ -4752,6 +4859,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -4760,12 +4868,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.2.tgz", - "integrity": "sha512-/Havz3PkYIEmwpqkyRTR21yJsWnFbD1ec4H1pUL+TkDnE7RCQkAVUQepLL/UeCaZeCBXvfdoKbOjSbV01xIinQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.5.tgz", + "integrity": "sha512-ILEzC2eyxx6ncej3zZSwMpB5RJ0zuqH7eMptxC4KN3f+v9bqT8ohssKbhNR78k/2tWW+KS5Spw+tbPF4Ejyqvw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.0.2", - "@smithy/types": "^3.2.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4773,16 +4882,17 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.0.3.tgz", - "integrity": "sha512-ARAXHodhj4tttKa9y75zvENdSoHq6VGsSi7XS3+yLutrnxttJs6N10UMInCC1yi3/bopT8xug3iOP/y9R6sKJQ==", - "dependencies": { - "@smithy/middleware-serde": "^3.0.2", - "@smithy/node-config-provider": "^3.1.2", - "@smithy/shared-ini-file-loader": "^3.1.2", - "@smithy/types": "^3.2.0", - "@smithy/url-parser": "^3.0.2", - "@smithy/util-middleware": "^3.0.2", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.0.tgz", + "integrity": "sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-middleware": "^3.0.3", "tslib": "^2.6.2" }, "engines": { @@ -4790,17 +4900,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.6.tgz", - "integrity": "sha512-ICsFKp8eAyIMmxN5UT3IU37S6886L879TKtgxPsn/VD/laYNwqTLmJaCAn5//+2fRIrV0dnHp6LFlMwdXlWoUQ==", - "dependencies": { - "@smithy/node-config-provider": "^3.1.2", - "@smithy/protocol-http": "^4.0.2", - "@smithy/service-error-classification": "^3.0.2", - "@smithy/smithy-client": "^3.1.4", - "@smithy/types": "^3.2.0", - "@smithy/util-middleware": "^3.0.2", - "@smithy/util-retry": "^3.0.2", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.15.tgz", + "integrity": "sha512-iTMedvNt1ApdvkaoE8aSDuwaoc+BhvHqttbA/FO4Ty+y/S5hW6Ci/CTScG7vam4RYJWZxdTElc3MEfHRVH6cgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "tslib": "^2.6.2", "uuid": "^9.0.1" }, @@ -4816,16 +4927,18 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/@smithy/middleware-serde": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.2.tgz", - "integrity": "sha512-oT2abV5zLhBucJe1LIIFEcRgIBDbZpziuMPswTMbBQNcaEUycLFvX63zsFmqfwG+/ZQKsNx+BSE8W51CMuK7Yw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", + "integrity": "sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4833,11 +4946,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.2.tgz", - "integrity": "sha512-6fRcxomlNKBPIy/YjcnC7YHpMAjRvGUYlYVJAfELqZjkW0vQegNcImjY7T1HgYA6u3pAcCxKVBLYnkTw8z/l0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.3.tgz", + "integrity": "sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4845,13 +4959,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.2.tgz", - "integrity": "sha512-388fEAa7+6ORj/BDC70peg3fyFBTTXJyXfXJ0Bwd6FYsRltePr2oGzIcm5AuC1WUSLtZ/dF+hYOnfTMs04rLvA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.4.tgz", + "integrity": "sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^3.1.2", - "@smithy/shared-ini-file-loader": "^3.1.2", - "@smithy/types": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4859,14 +4974,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.0.tgz", - "integrity": "sha512-pOpgB6B+VLXLwAyyvRz+ZAVXABlbAsJ2xvn3WZvrppAPImxwQOPFbeSUzWYMhpC8Tr7yQ3R8fG990QDhskkf1Q==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.4.tgz", + "integrity": "sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^3.1.0", - "@smithy/protocol-http": "^4.0.2", - "@smithy/querystring-builder": "^3.0.2", - "@smithy/types": "^3.2.0", + "@smithy/abort-controller": "^3.1.1", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4874,11 +4990,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.2.tgz", - "integrity": "sha512-Hzp32BpeFFexBpO1z+ts8okbq/VLzJBadxanJAo/Wf2CmvXMBp6Q/TLWr7Js6IbMEcr0pDZ02V3u1XZkuQUJaA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.3.tgz", + "integrity": "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4886,11 +5003,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.0.2.tgz", - "integrity": "sha512-X/90xNWIOqSR2tLUyWxVIBdatpm35DrL44rI/xoeBWUuanE0iyCXJpTcnqlOpnEzgcu0xCKE06+g70TTu2j7RQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.0.tgz", + "integrity": "sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4898,11 +5016,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.2.tgz", - "integrity": "sha512-xhv1+HacDYsOLdNt7zW+8Fe779KYAzmWvzs9bC5NlKM8QGYCwwuFwDBynhlU4D5twgi2pZ14Lm4h6RiAazCtmA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz", + "integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "@smithy/util-uri-escape": "^3.0.0", "tslib": "^2.6.2" }, @@ -4911,11 +5030,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.2.tgz", - "integrity": "sha512-C5hyRKgrZGPNh5QqIWzXnW+LXVrPmVQO0iJKjHeb5v3C61ZkP9QhrKmbfchcTyg/VnaE0tMNf/nmLpQlWuiqpg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.3.tgz", + "integrity": "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4923,22 +5043,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.2.tgz", - "integrity": "sha512-cu0WV2XRttItsuXlcM0kq5MKdphbMMmSd2CXF122dJ75NrFE0o7rruXFGfxAp3BKzgF/DMxX+PllIA/cj4FHMw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.3.tgz", + "integrity": "sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0" + "@smithy/types": "^3.3.0" }, "engines": { "node": ">=16.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.2.tgz", - "integrity": "sha512-tgnXrXbLMO8vo6VeuqabMw/eTzQHlLmZx0TC0TjtjJghnD0Xl4pEnJtBjTJr6XF5fHMNrt5BcczDXHJT9yNQnA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.4.tgz", + "integrity": "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -4946,14 +5068,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-3.1.1.tgz", - "integrity": "sha512-2/vlG86Sr489XX8TA/F+VDA+P04ESef04pSz0wRtlQBExcSPjqO08rvrkcas2zLnJ51i+7ukOURCkgqixBYjSQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.1.0.tgz", + "integrity": "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag==", + "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", - "@smithy/types": "^3.2.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", - "@smithy/util-middleware": "^3.0.2", + "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -4963,15 +5087,16 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.1.4.tgz", - "integrity": "sha512-y6xJROGrIoitjpwXLY7P9luDHvuT9jWpAluliuSFdBymFxcl6iyQjo9U/JhYfRHFNTruqsvKOrOESVuPGEcRmQ==", - "dependencies": { - "@smithy/middleware-endpoint": "^3.0.3", - "@smithy/middleware-stack": "^3.0.2", - "@smithy/protocol-http": "^4.0.2", - "@smithy/types": "^3.2.0", - "@smithy/util-stream": "^3.0.4", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.2.0.tgz", + "integrity": "sha512-pDbtxs8WOhJLJSeaF/eAbPgXg4VVYFlRcL/zoNYA5WbG3wBL06CHtBSg53ppkttDpAJ/hdiede+xApip1CwSLw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", "tslib": "^2.6.2" }, "engines": { @@ -4979,9 +5104,10 @@ } }, "node_modules/@smithy/types": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.2.0.tgz", - "integrity": "sha512-cKyeKAPazZRVqm7QPvcPD2jEIt2wqDPAL1KJKb0f/5I7uhollvsWZuZKLclmyP6a+Jwmr3OV3t+X0pZUUHS9BA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -4990,12 +5116,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.2.tgz", - "integrity": "sha512-pRiPHrgibeAr4avtXDoBHmTLtthwA4l8jKYRfZjNgp+bBPyxDMPRg2TMJaYxqbKemvrOkHu9MIBTv2RkdNfD6w==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.3.tgz", + "integrity": "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==", + "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^3.0.2", - "@smithy/types": "^3.2.0", + "@smithy/querystring-parser": "^3.0.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, @@ -5003,6 +5130,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", @@ -5016,6 +5144,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" } @@ -5024,6 +5153,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -5035,6 +5165,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" @@ -5047,6 +5178,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -5055,13 +5187,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.6.tgz", - "integrity": "sha512-tAgoc++Eq+KL7g55+k108pn7nAob3GLWNEMbXhZIQyBcBNaE/o3+r4AEbae0A8bWvLRvArVsjeiuhMykGa04/A==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.15.tgz", + "integrity": "sha512-FZ4Psa3vjp8kOXcd3HJOiDPBCWtiilLl57r0cnNtq/Ga9RSDrM5ERL6xt+tO43+2af6Pn5Yp92x2n5vPuduNfg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^3.1.2", - "@smithy/smithy-client": "^3.1.4", - "@smithy/types": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -5070,16 +5203,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.6.tgz", - "integrity": "sha512-UNerul6/E8aiCyFTBHk+RSIZCo7m96d/N5K3FeO/wFeZP6oy5HAicLzxqa85Wjv7MkXSxSySX29L/LwTV/QMag==", - "dependencies": { - "@smithy/config-resolver": "^3.0.3", - "@smithy/credential-provider-imds": "^3.1.2", - "@smithy/node-config-provider": "^3.1.2", - "@smithy/property-provider": "^3.1.2", - "@smithy/smithy-client": "^3.1.4", - "@smithy/types": "^3.2.0", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.15.tgz", + "integrity": "sha512-KSyAAx2q6d0t6f/S4XB2+3+6aQacm3aLMhs9aLMqn18uYGUepbdssfogW5JQZpc6lXNBnp0tEnR5e9CEKmEd7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^3.0.5", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -5087,12 +5221,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.0.3.tgz", - "integrity": "sha512-Dyi+pfLglDHSGsKSYunuUUSFM5V0tz7UDgv1Ex97yg+Xkn0Eb0rH0rcvl1n0MaJ11fac3HKDOH0DkALyQYCQag==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.0.5.tgz", + "integrity": "sha512-ReQP0BWihIE68OAblC/WQmDD40Gx+QY1Ez8mTdFMXpmjfxSyz2fVQu3A4zXRfQU9sZXtewk3GmhfOHswvX+eNg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.2", - "@smithy/types": "^3.2.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -5103,6 +5238,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -5111,11 +5247,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.2.tgz", - "integrity": "sha512-7WW5SD0XVrpfqljBYzS5rLR+EiDzl7wCVJZ9Lo6ChNFV4VYDk37Z1QI5w/LnYtU/QKnSawYoHRd7VjSyC8QRQQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.3.tgz", + "integrity": "sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.2.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -5123,12 +5260,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.2.tgz", - "integrity": "sha512-HUVOb1k8p/IH6WFUjsLa+L9H1Zi/FAAB2CDOpWuffI1b2Txi6sknau8kNfC46Xrt39P1j2KDzCE1UlLa2eW5+A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.3.tgz", + "integrity": "sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==", + "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^3.0.2", - "@smithy/types": "^3.2.0", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -5136,13 +5274,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.0.4.tgz", - "integrity": "sha512-CcMioiaOOsEVdb09pS7ux1ij7QcQ2jE/cE1+iin1DXMeRgAEQN/47m7Xztu7KFQuQsj0A5YwB2UN45q97CqKCg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.3.tgz", + "integrity": "sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^3.1.0", - "@smithy/node-http-handler": "^3.1.0", - "@smithy/types": "^3.2.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/types": "^3.3.0", "@smithy/util-base64": "^3.0.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-hex-encoding": "^3.0.0", @@ -5157,6 +5296,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -5168,6 +5308,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" @@ -5429,7 +5570,8 @@ "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" }, "node_modules/@tsconfig/node10": { "version": "1.0.9", @@ -5555,6 +5697,7 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "license": "MIT", "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -5660,7 +5803,6 @@ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "form-data": "^4.0.0" @@ -5676,6 +5818,12 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, "node_modules/@types/react": { "version": "17.0.80", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.80.tgz", @@ -5783,7 +5931,8 @@ "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "license": "MIT" }, "node_modules/@types/uuid": { "version": "8.3.4", @@ -5829,6 +5978,7 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" @@ -6069,52 +6219,26 @@ } }, "node_modules/@vscode/test-electron/node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dev": true, - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vscode/test-electron/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true - }, - "node_modules/@vscode/test-electron/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@vscode/test-electron/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", "dev": true, "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" + "restore-cursor": "^4.0.0" }, "engines": { - "node": ">= 14" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@vscode/test-electron/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, "node_modules/@vscode/test-electron/node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -6762,7 +6886,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -6776,7 +6899,6 @@ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "dev": true, - "license": "MIT", "dependencies": { "humanize-ms": "^1.2.1" }, @@ -7032,7 +7154,6 @@ "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", "dev": true, - "license": "MIT", "dependencies": { "object.assign": "^4.1.4", "util": "^0.10.4" @@ -7042,15 +7163,13 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/assert/node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", "dev": true, - "license": "MIT", "dependencies": { "inherits": "2.0.3" } @@ -7068,6 +7187,7 @@ "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -7165,7 +7285,6 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", "dev": true, - "license": "MIT", "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -7175,8 +7294,7 @@ "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/balanced-match": { "version": "1.0.2", @@ -7206,6 +7324,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -7358,12 +7477,14 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "license": "MIT", "optional": true }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" }, "node_modules/bplist-parser": { "version": "0.2.0", @@ -7380,6 +7501,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7503,7 +7625,6 @@ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", "dev": true, - "license": "MIT", "dependencies": { "pako": "~1.0.5" } @@ -7674,8 +7795,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/bundle-name": { "version": "3.0.0", @@ -7724,25 +7844,10 @@ "node": ">=8" } }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -7976,17 +8081,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/cli": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/cli/-/cli-0.4.5.tgz", - "integrity": "sha512-dbn5HyeJWSOU58RwOEiF1VWrl7HRvDsKLpu0uiI/vExH6iNoyUzjB5Mr3IJY5DVUfnbpe9793xw4DFJVzC9nWQ==", - "dependencies": { - "glob": ">= 3.1.4" - }, - "engines": { - "node": ">=0.2.5" - } - }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -8029,19 +8123,6 @@ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "dev": true }, - "node_modules/cliff": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/cliff/-/cliff-0.1.10.tgz", - "integrity": "sha512-roZWcC2Cxo/kKjRXw7YUpVNtxJccbvcl7VzTjUYgLQk6Ot0R8bm2netbhSZYWWNrKlOO/7HD6GXHl8dtzE6SiQ==", - "dependencies": { - "colors": "~1.0.3", - "eyes": "~0.1.8", - "winston": "0.8.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -8131,6 +8212,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "optional": true, "engines": { "node": ">=0.1.90" } @@ -8160,13 +8242,59 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/compare-module-exports/-/compare-module-exports-2.1.0.tgz", "integrity": "sha512-3Lc0sTIuX1jmY2K2RrXRJOND6KsRTX2D4v3+eu1PDptsuJZVK4LZc852eZa9I+avj0NrUKlTNgqvccNOH6mbGg==", - "dev": true, - "license": "ISC" + "dev": true + }, + "node_modules/compass-preferences-model": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/compass-preferences-model/-/compass-preferences-model-2.27.0.tgz", + "integrity": "sha512-BbHSGfYqCLslalRofDLjlRIJOHTiNWwfaWROQOTMrtLmYSPYyfQ5bm6piu4j+k+/qupcqRbMYbQ3NCJViIw64w==", + "license": "SSPL", + "dependencies": { + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "bson": "^6.7.0", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "react": "^17.0.2", + "yargs-parser": "^21.1.1", + "zod": "^3.22.3" + } + }, + "node_modules/compass-preferences-model/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/compass-preferences-model/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/compass-preferences-model/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/concat-stream": { "version": "1.6.2", @@ -8235,8 +8363,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -8281,8 +8408,7 @@ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", "dev": true, - "hasInstallScript": true, - "license": "MIT" + "hasInstallScript": true }, "node_modules/core-util-is": { "version": "1.0.3", @@ -8502,18 +8628,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, - "node_modules/cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", "engines": { "node": ">= 14" } @@ -8942,7 +9061,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -8985,6 +9103,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -9162,6 +9281,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", "optional": true }, "node_modules/diff": { @@ -9256,7 +9376,6 @@ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.4", "npm": ">=1.2" @@ -9497,10 +9616,11 @@ } }, "node_modules/electron": { - "version": "29.4.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.2.tgz", - "integrity": "sha512-XyIkuWQguwY8hGtLg0j5Q4Fqphdbh0ctBsKCSVzJ/R7Z2+2WN/oQ1M+zYwchmfiDgiuL3EKkrBrfPdxXYdMr+A==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-30.4.0.tgz", + "integrity": "sha512-ric3KLPQ9anXYjtTDkj5NbEcXZqRUwqxrxTviIjLdMdHqd5O+hkSHEzXgbSJUOt+7uw+zZuybn9+IM9y7iEpqg==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", @@ -9519,13 +9639,20 @@ "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==" }, "node_modules/electron/node_modules/@types/node": { - "version": "20.14.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", - "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", + "version": "20.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", + "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", @@ -9607,6 +9734,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", "engines": { "node": ">=6" } @@ -9692,7 +9820,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -9704,7 +9831,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -9783,6 +9909,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT", "optional": true }, "node_modules/escalade": { @@ -9813,6 +9940,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -9833,6 +9961,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -10835,6 +10964,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -10850,28 +10980,6 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", - "engines": { - "node": "> 0.1.90" - } - }, "node_modules/facepaint": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/facepaint/-/facepaint-1.2.1.tgz", @@ -10924,19 +11032,20 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", - "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - }, { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } ], + "license": "MIT", "dependencies": { "strnum": "^1.0.5" }, @@ -10984,6 +11093,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -11325,15 +11435,13 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", "dev": true, - "license": "MIT", "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" @@ -11347,7 +11455,6 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", "dev": true, - "license": "MIT", "engines": { "node": ">= 14" } @@ -11356,6 +11463,7 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" }, @@ -11389,6 +11497,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -11437,7 +11546,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.2", @@ -11523,7 +11633,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -11538,6 +11647,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -11558,6 +11682,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -11572,6 +11697,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -11585,6 +11711,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -11596,6 +11723,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -11610,6 +11738,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11647,6 +11776,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", "optional": true, "dependencies": { "boolean": "^3.0.1", @@ -11815,9 +11945,10 @@ } }, "node_modules/hadron-app-registry": { - "version": "9.1.12", - "resolved": "https://registry.npmjs.org/hadron-app-registry/-/hadron-app-registry-9.1.12.tgz", - "integrity": "sha512-RADslfdnH9uBx5wpdoP3ncvo5ujCUnzwDtrfLxPubmKkms3RsCSqZju+khWxiPxJi2+j6cqxm8gnPhqoKSdjiA==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/hadron-app-registry/-/hadron-app-registry-9.2.3.tgz", + "integrity": "sha512-jCwc2juLH143zEnXQAbyp54PZilqNUuDsMDDpAu5P4cGs23byQkwXtuwqzJ5rYkoTgjR387u+gqc8qSGfwH81A==", + "license": "SSPL", "dependencies": { "eventemitter3": "^4.0.0", "react": "^17.0.2", @@ -11829,12 +11960,14 @@ "node_modules/hadron-app-registry/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/hadron-app-registry/node_modules/react-redux": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -11869,14 +12002,6 @@ } } }, - "node_modules/hadron-app-registry/node_modules/react-redux/node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/hadron-document": { "version": "8.5.4", "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.5.4.tgz", @@ -11889,12 +12014,13 @@ } }, "node_modules/hadron-ipc": { - "version": "3.2.16", - "resolved": "https://registry.npmjs.org/hadron-ipc/-/hadron-ipc-3.2.16.tgz", - "integrity": "sha512-sBC0vGHYY3FcnbBY2OedIghSPFgNikLcjt7ayPzIEh35Y9PGJdmJ9popkTDzVEDJRuLP4tPRv5Q50Z+6Fkfigg==", + "version": "3.2.21", + "resolved": "https://registry.npmjs.org/hadron-ipc/-/hadron-ipc-3.2.21.tgz", + "integrity": "sha512-imPMxfqUYGmHLiwvALUs05NHs+zWywYAgps1xETAQAk/etlZmw3rkisestceRVq3BmFo78IueoeMmHaB0uQF7A==", + "license": "SSPL", "dependencies": { "debug": "^4.3.4", - "electron": "^29.4.2", + "electron": "^30.4.0", "is-electron-renderer": "^2.0.1" } }, @@ -11938,7 +12064,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -12085,6 +12210,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } @@ -12092,7 +12218,8 @@ "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/homedir-polyfill": { "version": "1.0.3", @@ -12213,7 +12340,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -12239,14 +12365,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.0.2", @@ -12269,7 +12393,6 @@ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.0.0" } @@ -12373,7 +12496,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -12382,6 +12504,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -12669,6 +12792,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz", "integrity": "sha512-Bm6H79i01DjgGTCWjUuCjJ6QDo1HB96PT/xCYuyJUP9WFbVDrLSbG4EZCvOCun2rNswZb0c3e4Jt/ws795esHA==", + "license": "MIT", "optional": true }, "node_modules/is-arguments": { @@ -13129,11 +13253,6 @@ "node": ">=0.10.0" } }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, "node_modules/jackspeak": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", @@ -13317,6 +13436,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", "optional": true }, "node_modules/json5": { @@ -13340,6 +13460,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -13648,8 +13769,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/lodash.clonedeep": { "version": "4.5.0", @@ -13714,15 +13834,13 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", "dev": true, - "license": "MIT", "dependencies": { "lodash._reinterpolate": "^3.0.0", "lodash.templatesettings": "^4.0.0" @@ -13733,7 +13851,6 @@ "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", "dev": true, - "license": "MIT", "dependencies": { "lodash._reinterpolate": "^3.0.0" } @@ -13803,6 +13920,7 @@ "resolved": "https://registry.npmjs.org/macos-export-certificate-and-key/-/macos-export-certificate-and-key-1.2.2.tgz", "integrity": "sha512-+LwU/wG3wawI3yZ/CMf9C6jSSugJ823EuNJeV8J+FTbmYDJ8G3sF9Fha/0BLEbRZU28+oVvBD3a4mYxLQzDvLA==", "hasInstallScript": true, + "license": "Apache-2.0", "optional": true, "os": [ "darwin" @@ -13886,6 +14004,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", "optional": true, "dependencies": { "escape-string-regexp": "^4.0.0" @@ -14088,6 +14207,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14517,52 +14637,56 @@ } }, "node_modules/mongodb-data-service": { - "version": "22.21.1", - "resolved": "https://registry.npmjs.org/mongodb-data-service/-/mongodb-data-service-22.21.1.tgz", - "integrity": "sha512-zaelO1uF3VYqExUfsREgLp55EGLmhKsIRLxqufiGTLLgzV9jXmUMbPiAKxNuo5Fu0IOAwzV0D38RfZdB1yJVVQ==", - "dependencies": { - "@mongodb-js/compass-logging": "^1.3.1", - "@mongodb-js/compass-utils": "^0.6.5", - "@mongodb-js/devtools-connect": "^3.0.1", - "@mongodb-js/ssh-tunnel": "^2.2.1", + "version": "22.23.0", + "resolved": "https://registry.npmjs.org/mongodb-data-service/-/mongodb-data-service-22.23.0.tgz", + "integrity": "sha512-JYrYy3ureElrzxVzA7plT0NYL1Sei0rQYGvQm9BMgKx+DYy7EEB5/gzjUASQ1LkVfRoaBTgjUARDhlWLuCnWog==", + "license": "SSPL", + "dependencies": { + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/devtools-connect": "^3.2.5", + "@mongodb-js/devtools-proxy-support": "^0.3.6", "bson": "^6.7.0", + "compass-preferences-model": "^2.27.0", "lodash": "^4.17.21", - "mongodb": "^6.7.0", + "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", "mongodb-connection-string-url": "^3.0.1", "mongodb-ns": "^2.4.2" }, "optionalDependencies": { - "mongodb-client-encryption": "^6.0.0" + "mongodb-client-encryption": "~6.0.1" } }, "node_modules/mongodb-data-service/node_modules/@mongodb-js/devtools-connect": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.0.1.tgz", - "integrity": "sha512-xbk/eGHPQTQz4VSpGb5oRqSSbzipcFDODrAc4YtYFrb0980buOAopO71NozCbQoVnoiO1pYVIqcnrZMHkdaJzg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.6.tgz", + "integrity": "sha512-E5jvGDHZ13fnDkuIytnINIS2/2BR0aiC0rfXLKeOO6ongJfL8F5ACEz5dbCR+e6eJ4JCeh1Tb49CfjZK9iGhWQ==", + "license": "Apache-2.0", "dependencies": { - "@mongodb-js/oidc-http-server-pages": "1.1.1", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/oidc-http-server-pages": "1.1.2", "lodash.merge": "^4.6.2", "mongodb-connection-string-url": "^3.0.0", - "socks": "^2.7.3", - "system-ca": "^1.0.2" + "socks": "^2.7.3" }, "optionalDependencies": { "kerberos": "^2.1.0", - "mongodb-client-encryption": "^6.0.0", + "mongodb-client-encryption": "^6.0.0 || ^6.1.0-alpha.0", "os-dns-native": "^1.2.0", "resolve-mongodb-srv": "^1.1.1" }, "peerDependencies": { - "@mongodb-js/oidc-plugin": "^1.0.0", - "mongodb": "^5.8.1 || ^6.0.0", + "@mongodb-js/oidc-plugin": "^1.1.0", + "mongodb": "^6.8.0", "mongodb-log-writer": "^1.4.2" } }, "node_modules/mongodb-data-service/node_modules/@mongodb-js/oidc-plugin": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.0.2.tgz", - "integrity": "sha512-hwTbkmJ31RPB5ksA6pLepnaQOBz6iurE+uH89B1IIJdxVuiO0Qz+OqpTN8vk8LZzcVDb/WbNoxqxogCWwMqFKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", + "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", + "license": "Apache-2.0", "peer": true, "dependencies": { "express": "^4.18.2", @@ -14603,12 +14727,13 @@ "integrity": "sha512-gYJjEYG4v4a1WSXgUf81OBoBRlj+Z1SlnQVO392fC/4a1CN7CLWDITajZWPFTPh/yRozYk6sHHtZwZmQhodBEA==" }, "node_modules/mongodb-query-parser": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/mongodb-query-parser/-/mongodb-query-parser-4.1.3.tgz", - "integrity": "sha512-6V439TLqpuQ5c/vsUVuPxLbmkzFX+LrQKveWrlx0Q6HfbukGmgMtezpYNdCp0SwOaEWTnwJPKI4GQxQdyHGDLg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/mongodb-query-parser/-/mongodb-query-parser-4.2.2.tgz", + "integrity": "sha512-G97DZawo9f6uTdqIcTR+79sL7cPNS4AHNIGFEyzaWlAo/6bRI2Ihd3f/fZgtrzgCgWl0CgKE9S5eywXSpuaggQ==", + "license": "Apache-2.0", "dependencies": { + "@mongodb-js/shell-bson-parser": "^1.1.1", "debug": "^4.3.4", - "ejson-shell-parser": "^2.0.0", "javascript-stringify": "^2.1.0", "lodash": "^4.17.21" } @@ -14617,6 +14742,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-1.1.3.tgz", "integrity": "sha512-rMw3JlgJ1WhTYUlztojIJ+PGO4sQuwQmsbs1YY60Qf+jPHiDfP10YF//BvpVfy5lKRxAYZmgRTL/fLj1bUbIAg==", + "license": "Apache-2.0", "dependencies": { "lodash": "^4.17.21" } @@ -14856,6 +14982,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -14926,6 +15053,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "engines": { "node": ">=10.5.0" } @@ -14968,22 +15096,11 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "optional": true, - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", "dev": true, - "license": "MIT", "dependencies": { "assert": "^1.1.1", "browserify-zlib": "^0.2.0", @@ -15015,7 +15132,6 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", "dev": true, - "license": "MIT", "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", @@ -15026,29 +15142,25 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/node-libs-browser/node_modules/path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/node-libs-browser/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/node-libs-browser/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15063,15 +15175,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/node-libs-browser/node_modules/readable-stream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -15081,7 +15191,6 @@ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dev": true, - "license": "MIT", "dependencies": { "inherits": "~2.0.1", "readable-stream": "^2.0.2" @@ -15092,7 +15201,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } @@ -15358,7 +15466,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -15526,19 +15633,20 @@ } }, "node_modules/openai": { - "version": "4.55.7", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.55.7.tgz", - "integrity": "sha512-I2dpHTINt0Zk+Wlns6KzkKu77MmNW3VfIIQf5qYziEUI6t7WciG1zTobfKqdPzBmZi3TTM+3DtjPumxQdcvzwA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.57.0.tgz", + "integrity": "sha512-JnwBSIYqiZ3jYjB5f2in8hQ0PRA092c6m+/6dYB0MzK0BEbn+0dioxZsPLBm5idJbg9xzLNOiGVm2OSuhZ+BdQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", + "@types/qs": "^6.9.7", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" + "node-fetch": "^2.6.7", + "qs": "^6.10.3" }, "bin": { "openai": "bin/cli" @@ -15553,11 +15661,10 @@ } }, "node_modules/openai/node_modules/@types/node": { - "version": "18.19.44", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.44.tgz", - "integrity": "sha512-ZsbGerYg72WMXUIE9fYxtvfzLEuq6q8mKERdWFnqTmOvudMxnz+CBNRoOwJ2kNpFOncrKjT1hZwxjlFgQ9qvQA==", + "version": "18.19.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz", + "integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -15703,14 +15810,14 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/os-dns-native": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/os-dns-native/-/os-dns-native-1.2.1.tgz", "integrity": "sha512-LbU43lWBxnZhy72Ngr+Vga0og5Q2+Ob8lvSHJkP2uYBkvdmAnK4CvaVaBhC1hk9AQV3YxAZ9fZWaJTuIyPEi+Q==", "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { "bindings": "^1.5.0", @@ -15785,6 +15892,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.0.2", @@ -15799,45 +15907,11 @@ "node": ">= 14" } }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/pac-resolver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -15978,6 +16052,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -16148,14 +16223,6 @@ "node": ">=8" } }, - "node_modules/pkginfo": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", - "integrity": "sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -16856,6 +16923,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.9.2" } @@ -16864,6 +16932,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/reflux/-/reflux-0.4.1.tgz", "integrity": "sha512-xrAUNOKzNegiYHpj9Vv5eCdV3HZ1H0f9T5TXcTVk1dbOSMBL4ppUM+3Gvw1HCiLRfe+IAtj7k4DXe/r7XcswlQ==", + "license": "BSD-3-Clause", "dependencies": { "eventemitter3": "^1.1.1", "reflux-core": "^0.3.0" @@ -16876,6 +16945,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/reflux-core/-/reflux-core-0.3.0.tgz", "integrity": "sha512-mOOxvUQcNE3HvYXHt2SoqlVso5OtQL82zl60PfR1An57UU4ho30h0hMO6fh5hsDcfxywo74+TdForH+pfCZGeQ==", + "license": "BSD-3-Clause", "dependencies": { "eventemitter3": "^1.1.1" } @@ -16883,12 +16953,14 @@ "node_modules/reflux-core/node_modules/eventemitter3": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", - "integrity": "sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA==" + "integrity": "sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA==", + "license": "MIT" }, "node_modules/reflux/node_modules/eventemitter3": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", - "integrity": "sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA==" + "integrity": "sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA==", + "license": "MIT" }, "node_modules/regenerator-runtime": { "version": "0.13.11", @@ -17023,6 +17095,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/resolve-mongodb-srv/-/resolve-mongodb-srv-1.1.5.tgz", "integrity": "sha512-flu1XTSLDJHvTnWu2aJh2w9jgGPcNYJn2obMkuzXiyWSz0MLXu9IRCjvirJ4zRoCPHJJPt3uLQVNJTrzFRWd1w==", + "license": "Apache-2.0", "optional": true, "dependencies": { "whatwg-url": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" @@ -17031,40 +17104,6 @@ "resolve-mongodb-srv": "bin/resolve-mongodb-srv.js" } }, - "node_modules/resolve-mongodb-srv/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "optional": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-mongodb-srv/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "optional": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-mongodb-srv/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "optional": true, - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -17104,7 +17143,6 @@ "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.5.tgz", "integrity": "sha512-MdPutvaUd+kKVz/lcEz6N6337s4PxRUR5vhphIp2/TJRgfXIckomIkCsIAbwB53MjiSLwi7KBMdQ9lPWE5WpYA==", "dev": true, - "license": "MIT", "dependencies": { "babel-runtime": "^6.26.0", "compare-module-exports": "^2.1.0", @@ -17145,6 +17183,7 @@ "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", "optional": true, "dependencies": { "boolean": "^3.0.1", @@ -17162,6 +17201,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/rrweb-cssom": { @@ -17473,6 +17513,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", "optional": true, "dependencies": { "type-fest": "^0.13.1" @@ -17484,18 +17525,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -17523,7 +17552,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -17617,7 +17645,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -17864,6 +17891,7 @@ "version": "8.0.4", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", @@ -17873,55 +17901,6 @@ "node": ">= 14" } }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socksv5": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/socksv5/-/socksv5-0.0.6.tgz", - "integrity": "sha512-tQpQ0MdNQAsQBDhCXy3OvGGJikh9QOl3PkbwT4POJiQCm/fK4z9AxKQQRG8WLeF6talphnPrSWiZRpTl42rApg==", - "bundleDependencies": [ - "ipv6" - ], - "dependencies": { - "ipv6": "*" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/socksv5/node_modules/ipv6": { - "version": "3.1.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cli": "0.4.x", - "cliff": "0.1.x", - "sprintf": "0.1.x" - }, - "bin": { - "ipv6": "bin/ipv6.js", - "ipv6grep": "bin/ipv6grep.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/socksv5/node_modules/ipv6/node_modules/sprintf": { - "version": "0.1.3", - "inBundle": true, - "engines": { - "node": ">=0.2.4" - } - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -18066,14 +18045,6 @@ "nan": "^2.18.0" } }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "engines": { - "node": "*" - } - }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -18208,7 +18179,6 @@ "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", "dev": true, - "license": "MIT", "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", @@ -18221,15 +18191,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/stream-http/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -18244,15 +18212,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/stream-http/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -18432,7 +18398,8 @@ "node_modules/strnum": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "license": "MIT" }, "node_modules/style-mod": { "version": "4.1.0", @@ -18448,6 +18415,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" }, @@ -18484,12 +18452,13 @@ "dev": true }, "node_modules/system-ca": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/system-ca/-/system-ca-1.0.3.tgz", - "integrity": "sha512-FHwdyDHM/J6SjHNVEp532J5rWsO0oPB/szZGsP/8I2s78mPLQhE0WwKTGi/RJulqTICekINsczg5q44n/1b+lQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/system-ca/-/system-ca-2.0.1.tgz", + "integrity": "sha512-9ZDV9yl8ph6Op67wDGPr4LykX86usE9x3le+XZSHfVMiiVJ5IRgmCWjLgxyz35ju9H3GDIJJZm4ogAeIfN5cQQ==", + "license": "Apache-2.0", "optionalDependencies": { - "macos-export-certificate-and-key": "^1.1.1", - "win-export-certificate-and-key": "^1.1.1" + "macos-export-certificate-and-key": "^1.2.0", + "win-export-certificate-and-key": "^2.1.0" } }, "node_modules/tabbable": { @@ -18871,7 +18840,6 @@ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", "dev": true, - "license": "MIT", "dependencies": { "setimmediate": "^1.0.4" }, @@ -18908,8 +18876,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/to-buffer": { "version": "1.1.1", @@ -18981,7 +18948,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -19115,8 +19082,7 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/tunnel": { "version": "0.0.6", @@ -19165,6 +19131,19 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -19285,7 +19264,8 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "node_modules/universal-user-agent": { "version": "6.0.1", @@ -19297,6 +19277,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", "engines": { "node": ">= 4.0.0" } @@ -19360,7 +19341,6 @@ "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", "dev": true, - "license": "MIT", "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" @@ -19389,15 +19369,13 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/url/node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" }, @@ -19408,12 +19386,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, - "license": "MIT", "dependencies": { "inherits": "2.0.3" } @@ -19427,8 +19413,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/utils-merge": { "version": "1.0.1", @@ -19474,8 +19459,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/vscode-jsonrpc": { "version": "8.1.0", @@ -19591,6 +19575,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", "engines": { "node": ">= 8" } @@ -19803,7 +19788,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tr46": "^5.0.0", @@ -19880,70 +19865,17 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "node_modules/win-export-certificate-and-key": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/win-export-certificate-and-key/-/win-export-certificate-and-key-1.1.2.tgz", - "integrity": "sha512-3Su7Xdt9UR8pZicWQDcGSWNL6We/NzAGxe7AeQ1Z/zAeGHWlTKAh3HemGvIvxLRPsk4NW9D/QL3cL6SIvFcTvQ==", - "hasInstallScript": true, - "optional": true, - "os": [ - "win32" - ], - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^3.1.0", - "node-forge": "^1.2.1" - } - }, - "node_modules/win-export-certificate-and-key/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "optional": true - }, - "node_modules/winston": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz", - "integrity": "sha512-fPoamsHq8leJ62D1M9V/f15mjQ1UHe4+7j1wpAT3fqgA5JqhJkk4aIfPEjfMTI9x6ZTjaLOpMAjluLtmgO5b6g==", - "dependencies": { - "async": "0.2.x", - "colors": "0.6.x", - "cycle": "1.0.x", - "eyes": "0.1.x", - "isstream": "0.1.x", - "pkginfo": "0.3.x", - "stack-trace": "0.0.x" - }, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/winston/node_modules/async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" - }, - "node_modules/winston/node_modules/colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/wipe-node-cache": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-2.1.2.tgz", "integrity": "sha512-m7NXa8qSxBGMtdQilOu53ctMaIBXy93FOP04EC1Uf4bpsE+r+adfLKwIMIvGbABsznaSNxK/ErD4xXDyY5og9w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/wipe-webpack-cache": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wipe-webpack-cache/-/wipe-webpack-cache-2.1.0.tgz", "integrity": "sha512-OXzQMGpA7MnQQ8AG+uMl5mWR2ezy6fw1+DMHY+wzYP1qkF1jrek87psLBmhZEj+er4efO/GD4R8jXWFierobaA==", "dev": true, - "license": "MIT", "dependencies": { "wipe-node-cache": "^2.1.0" } @@ -20015,6 +19947,31 @@ "node": ">=4" } }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/write/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -20265,6 +20222,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 1723d0397..7a0e7837d 100644 --- a/package.json +++ b/package.json @@ -1157,10 +1157,10 @@ "@mongodb-js/connection-form": "^1.22.2", "@mongodb-js/connection-info": "^0.1.2", "@mongodb-js/mongodb-constants": "^0.10.2", - "@mongosh/browser-runtime-electron": "^2.2.15", - "@mongosh/i18n": "^2.2.15", - "@mongosh/service-provider-server": "^2.2.15", - "@mongosh/shell-api": "^2.2.15", + "@mongosh/browser-runtime-electron": "^2.3.0", + "@mongosh/i18n": "^2.3.0", + "@mongosh/service-provider-server": "^2.3.0", + "@mongosh/shell-api": "^2.3.0", "@segment/analytics-node": "^1.3.0", "bson": "^6.8.0", "bson-transpilers": "^2.2.0", @@ -1173,9 +1173,9 @@ "mongodb-build-info": "^1.7.2", "mongodb-cloud-info": "^2.1.4", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.21.1", + "mongodb-data-service": "^22.23.0", "mongodb-log-writer": "^1.4.2", - "mongodb-query-parser": "^4.1.3", + "mongodb-query-parser": "^4.2.2", "mongodb-schema": "^12.2.0", "numeral": "^2.0.6", "react": "^18.3.1", diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index 50c1cf409..78f7d649d 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -958,9 +958,9 @@ suite('Connection Controller Test Suite', function () { options: { autoEncryption: undefined, monitorCommands: true, - useSystemCA: undefined, + applyProxyToOIDC: false, authMechanismProperties: {}, - oidc: {}, + oidc: { customHttpOptions: {} }, productDocsLink: 'https://docs.mongodb.com/mongodb-vscode/?utm_source=vscode&utm_medium=product', productName: 'mongodb-vscode', diff --git a/webpack.config.js b/webpack.config.js index 98b418c8c..fd928938f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -41,6 +41,8 @@ module.exports = (env, argv) => { // webpack would bring it inside the bundle otherwise. electron: false, + 'hadron-ipc': false, + // We don't currently support kerberos in our extension. kerberos: false, @@ -58,10 +60,7 @@ module.exports = (env, argv) => { // own keytar dependency. Here we are telling it to use vscode's keytar. keytar: 'keytar', - // Electron: - electron: 'electron', '@electron/remote': '@electron/remote', - 'hadron-ipc': 'hadron-ipc', // MongoDB node driver externals: snappy: 'snappy', @@ -70,7 +69,6 @@ module.exports = (env, argv) => { 'win-export-certificate-and-key': 'win-export-certificate-and-key', 'os-dns-native': 'os-dns-native', 'mongodb-client-encryption': 'mongodb-client-encryption', - 'compass-preferences-model': 'compass-preferences-model', '@mongodb-js/zstd': '@mongodb-js/zstd', 'gcp-metadata': 'gcp-metadata', encoding: 'encoding', From 633f1b78e68c323cb5535f451111db638bd3b4db Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Wed, 4 Sep 2024 09:54:29 +0200 Subject: [PATCH 14/45] feat: improve the format of the participant answer VSCODE-582 (#804) * feat: improve the format of the participant answer VSCODE-582 * refactor: move evaluateParticipantQuery back to playground controller * fix: use chat response in history when applicable --- README.md | 1 + package.json | 5 ++ src/editors/playgroundController.ts | 78 +++++++++++-------- src/mdbExtensionController.ts | 18 ++--- src/participant/participant.ts | 11 +-- src/participant/prompts/history.ts | 12 +-- src/templates/playgroundBasicTextTemplate.ts | 2 - .../editors/playgroundController.test.ts | 16 +--- ...aygroundSelectedCodeActionProvider.test.ts | 30 +------ .../language/languageServerController.test.ts | 7 -- .../suite/participant/participant.test.ts | 2 +- 11 files changed, 80 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 54ec00cc1..01a8fb61e 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ Connect to Atlas Stream Processing instances and develop stream processors using | `mdb.showMongoDBHelpExplorer` | Show or hide the MongoDB Help panel. | `true` | | `mdb.defaultLimit` | The number of documents to fetch when viewing documents from a collection. | `10` | | `mdb.confirmRunAll` | Show a confirmation message before running commands in a playground. | `true` | +| `mdb.confirmRunCopilotCode` | Show a confirmation message before running code generated by the MongoDB participant. | `true` | | `mdb.confirmDeleteDocument` | Show a confirmation message before deleting a document in the tree view. | `true` | | `mdb.persistOIDCTokens` | Remain logged in when using the MONGODB-OIDC authentication mechanism for MongoDB server connection. Access tokens are encrypted using the system keychain before being stored. | `true` | | `mdb.showOIDCDeviceAuthFlow` | Opt-in and opt-out for diagnostic and telemetry collection. | `true` | diff --git a/package.json b/package.json index 7a0e7837d..7ac90b042 100644 --- a/package.json +++ b/package.json @@ -1096,6 +1096,11 @@ "default": true, "description": "Show a confirmation message before running commands in a playground." }, + "mdb.confirmRunCopilotCode": { + "type": "boolean", + "default": true, + "description": "Show a confirmation message before running code generated by the MongoDB participant." + }, "mdb.confirmDeleteDocument": { "type": "boolean", "default": true, diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 3e152ed0f..0d82c9bc5 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -44,7 +44,6 @@ import { isPlayground, getPlaygroundExtensionForTelemetry, } from '../utils/playground'; -import type ParticipantController from '../participant/participant'; const log = createLogger('playground controller'); @@ -132,11 +131,8 @@ export default class PlaygroundController { private _playgroundResultTextDocument?: vscode.TextDocument; private _statusView: StatusView; private _playgroundResultViewProvider: PlaygroundResultProvider; - private _participantController: ParticipantController; private _activeConnectionChangedHandler: () => void; - private _codeToEvaluate = ''; - constructor({ connectionController, languageServerController, @@ -145,7 +141,6 @@ export default class PlaygroundController { playgroundResultViewProvider, exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider, - participantController, }: { connectionController: ConnectionController; languageServerController: LanguageServerController; @@ -154,7 +149,6 @@ export default class PlaygroundController { playgroundResultViewProvider: PlaygroundResultProvider; exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider; playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; - participantController: ParticipantController; }) { this._connectionController = connectionController; this._activeTextEditor = vscode.window.activeTextEditor; @@ -165,7 +159,6 @@ export default class PlaygroundController { this._exportToLanguageCodeLensProvider = exportToLanguageCodeLensProvider; this._playgroundSelectedCodeActionProvider = playgroundSelectedCodeActionProvider; - this._participantController = participantController; this._activeConnectionChangedHandler = () => { void this._activeConnectionChanged(); @@ -489,7 +482,9 @@ export default class PlaygroundController { return this._activeTextEditor?.document.getText(selection) || ''; } - async _evaluateWithCancelModal(): Promise { + async _evaluateWithCancelModal( + codeToEvaluate: string + ): Promise { if (!this._connectionController.isCurrentlyConnected()) { throw new Error( 'Please connect to a database before running a playground.' @@ -513,7 +508,7 @@ export default class PlaygroundController { // Run all playground scripts. const result: ShellEvaluateResult = await this._evaluate( - this._codeToEvaluate + codeToEvaluate ); return result; @@ -528,10 +523,8 @@ export default class PlaygroundController { } } - async _openPlaygroundResult(): Promise { - this._playgroundResultViewProvider.setPlaygroundResult( - this._playgroundResult - ); + async _openInResultPane(result: PlaygroundResult): Promise { + this._playgroundResultViewProvider.setPlaygroundResult(result); if (!this._playgroundResultTextDocument) { await this._openResultAsVirtualDocument(); @@ -542,7 +535,7 @@ export default class PlaygroundController { await this._showResultAsVirtualDocument(); if (this._playgroundResultTextDocument) { - const language = this._playgroundResult?.language || 'plaintext'; + const language = result?.language || 'plaintext'; await vscode.languages.setTextDocumentLanguage( this._playgroundResultTextDocument, @@ -579,7 +572,37 @@ export default class PlaygroundController { } } - async _evaluatePlayground(): Promise { + async evaluateParticipantQuery(text: string): Promise { + const shouldConfirmRunCopilotCode = vscode.workspace + .getConfiguration('mdb') + .get('confirmRunCopilotCode'); + + if (shouldConfirmRunCopilotCode === true) { + const name = this._connectionController.getActiveConnectionName(); + const confirmRunCopilotCode = await vscode.window.showInformationMessage( + `Are you sure you want to run this code generated by the MongoDB participant against ${name}? This confirmation can be disabled in the extension settings.`, + { modal: true }, + 'Yes' + ); + + if (confirmRunCopilotCode !== 'Yes') { + return false; + } + } + + const evaluateResponse: ShellEvaluateResult = + await this._evaluateWithCancelModal(text); + + if (!evaluateResponse || !evaluateResponse.result) { + return false; + } + + await this._openInResultPane(evaluateResponse.result); + + return Promise.resolve(true); + } + + async _evaluatePlayground(text: string): Promise { const shouldConfirmRunAll = vscode.workspace .getConfiguration('mdb') .get('confirmRunAll'); @@ -606,15 +629,14 @@ export default class PlaygroundController { } const evaluateResponse: ShellEvaluateResult = - await this._evaluateWithCancelModal(); + await this._evaluateWithCancelModal(text); if (!evaluateResponse || !evaluateResponse.result) { return false; } this._playgroundResult = evaluateResponse.result; - - await this._openPlaygroundResult(); + await this._openInResultPane(this._playgroundResult); return true; } @@ -629,9 +651,8 @@ export default class PlaygroundController { } this._isPartialRun = true; - this._codeToEvaluate = this._selectedText; - return this._evaluatePlayground(); + return this._evaluatePlayground(this._selectedText || ''); } runAllPlaygroundBlocks(): Promise { @@ -647,9 +668,8 @@ export default class PlaygroundController { } this._isPartialRun = false; - this._codeToEvaluate = this._getAllText(); - return this._evaluatePlayground(); + return this._evaluatePlayground(this._getAllText()); } runAllOrSelectedPlaygroundBlocks(): Promise { @@ -666,19 +686,20 @@ export default class PlaygroundController { const selections = this._activeTextEditor.selections; + let codeToEvaluate; if ( !selections || !Array.isArray(selections) || (selections.length === 1 && this._getSelectedText(selections[0]) === '') ) { this._isPartialRun = false; - this._codeToEvaluate = this._getAllText(); + codeToEvaluate = this._getAllText(); } else if (this._selectedText) { this._isPartialRun = true; - this._codeToEvaluate = this._selectedText; + codeToEvaluate = this._selectedText; } - return this._evaluatePlayground(); + return this._evaluatePlayground(codeToEvaluate); } async fixThisInvalidInteractiveSyntax({ @@ -815,11 +836,6 @@ export default class PlaygroundController { return { namespace, expression }; } - async evaluateParticipantQuery({ text }: { text: string }): Promise { - this._codeToEvaluate = text; - return this._evaluatePlayground(); - } - async _transpile(): Promise { const { selectedText, importStatements, driverSyntax, builders, language } = this._exportToLanguageCodeLensProvider._exportToLanguageAddons; @@ -896,7 +912,7 @@ export default class PlaygroundController { } /* eslint-enable camelcase */ - await this._openPlaygroundResult(); + await this._openInResultPane(this._playgroundResult); } catch (error) { log.error(`Export to the '${language}' language failed`, error); const printableError = formatError(error); diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 789ed84e5..ac88576dd 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -109,10 +109,6 @@ export default class MDBExtensionController implements vscode.Disposable { new PlaygroundSelectedCodeActionProvider(); this._playgroundDiagnosticsCodeActionProvider = new PlaygroundDiagnosticsCodeActionProvider(); - this._participantController = new ParticipantController({ - connectionController: this._connectionController, - storageController: this._storageController, - }); this._playgroundController = new PlaygroundController({ connectionController: this._connectionController, languageServerController: this._languageServerController, @@ -122,7 +118,10 @@ export default class MDBExtensionController implements vscode.Disposable { exportToLanguageCodeLensProvider: this._exportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: this._playgroundSelectedCodeActionProvider, - participantController: this._participantController, + }); + this._participantController = new ParticipantController({ + connectionController: this._connectionController, + storageController: this._storageController, }); this._editorsController = new EditorsController({ context, @@ -302,11 +301,10 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerParticipantCommand( EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, () => { - return this._playgroundController.evaluateParticipantQuery({ - text: - this._participantController._chatResult?.metadata - ?.responseContent || '', - }); + return this._playgroundController.evaluateParticipantQuery( + this._participantController._chatResult?.metadata?.responseContent || + '' + ); } ); this.registerCommand( diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 01d4c1742..8919ae565 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -60,7 +60,7 @@ export function getRunnableContentFromString(text: string) { matchedJSresponseContent && matchedJSresponseContent.length > 1 ? matchedJSresponseContent[1] : ''; - return code; + return code.trim(); } export default class ParticipantController { @@ -723,7 +723,7 @@ export default class ParticipantController { vscode.ChatResponseStream, vscode.CancellationToken ] - ): Promise { + ): Promise { const [request, , stream] = args; if (!request.prompt || request.prompt.trim().length === 0) { @@ -731,7 +731,7 @@ export default class ParticipantController { for (const msg of messages) { stream.markdown(msg); } - return; + return { metadata: {} }; } const hasBeenShownWelcomeMessageAlready = !!this._storageController.get( @@ -752,13 +752,14 @@ export default class ParticipantController { if (request.command === 'query') { this._chatResult = await this.handleQueryRequest(...args); - return; } else if (request.command === 'docs') { // TODO(VSCODE-570): Implement this. } else if (request.command === 'schema') { // TODO(VSCODE-571): Implement this. + } else { + this._chatResult = await this.handleGenericRequest(...args); } - this._chatResult = await this.handleGenericRequest(...args); + return this._chatResult; } } diff --git a/src/participant/prompts/history.ts b/src/participant/prompts/history.ts index 907838b7e..ea2c9d48d 100644 --- a/src/participant/prompts/history.ts +++ b/src/participant/prompts/history.ts @@ -18,13 +18,15 @@ export function getHistoryMessages({ messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt)); } - if ( - historyItem.participant === CHAT_PARTICIPANT_ID && - historyItem instanceof vscode.ChatResponseTurn - ) { + if (historyItem instanceof vscode.ChatResponseTurn) { let res = ''; for (const fragment of historyItem.response) { - res += fragment; + if ( + fragment instanceof vscode.ChatResponseMarkdownPart && + historyItem.result.metadata?.responseContent + ) { + res += fragment.value.value; + } } // eslint-disable-next-line new-cap messages.push(vscode.LanguageModelChatMessage.Assistant(res)); diff --git a/src/templates/playgroundBasicTextTemplate.ts b/src/templates/playgroundBasicTextTemplate.ts index 2fe3e8186..6114f5840 100644 --- a/src/templates/playgroundBasicTextTemplate.ts +++ b/src/templates/playgroundBasicTextTemplate.ts @@ -8,8 +8,6 @@ const template = `/* global use, db */ // Use 'console.log()' to print to the debug output. // For more documentation on playgrounds please refer to // https://www.mongodb.com/docs/mongodb-vscode/playgrounds/ -// Select the database to use. -use('CURRENT_DATABASE'); PLAYGROUND_CONTENT `; diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index fd3c7c910..7d5458161 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -21,7 +21,6 @@ import { StorageController } from '../../../storage'; import TelemetryService from '../../../telemetry/telemetryService'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { ExtensionContextStub, LanguageServerControllerStub } from '../stubs'; -import ParticipantController from '../../../participant/participant'; const expect = chai.expect; @@ -56,7 +55,6 @@ suite('Playground Controller Test Suite', function () { let testPlaygroundController: PlaygroundController; let showErrorMessageStub: SinonStub; let sandbox: sinon.SinonSandbox; - let testParticipantController: ParticipantController; beforeEach(() => { sandbox = sinon.createSandbox(); @@ -85,10 +83,6 @@ suite('Playground Controller Test Suite', function () { extensionContextStub, testStorageController ); - testParticipantController = new ParticipantController({ - connectionController: testConnectionController, - storageController: testStorageController, - }); testPlaygroundController = new PlaygroundController({ connectionController: testConnectionController, languageServerController: languageServerControllerStub, @@ -97,7 +91,6 @@ suite('Playground Controller Test Suite', function () { playgroundResultViewProvider: testPlaygroundResultProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, - participantController: testParticipantController, }); showErrorMessageStub = sandbox.stub(vscode.window, 'showErrorMessage'); sandbox.stub(testTelemetryService, 'trackNewConnection'); @@ -331,8 +324,9 @@ suite('Playground Controller Test Suite', function () { sandbox.fake.rejects(false) ); - const result = - await testPlaygroundController._evaluateWithCancelModal(); + const result = await testPlaygroundController._evaluateWithCancelModal( + '' + ); expect(result).to.deep.equal({ result: undefined }); }); @@ -353,7 +347,6 @@ suite('Playground Controller Test Suite', function () { exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, - participantController: testParticipantController, }); expect(playgroundController._activeTextEditor).to.deep.equal( @@ -371,7 +364,6 @@ suite('Playground Controller Test Suite', function () { exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, - participantController: testParticipantController, }); const textFromEditor = 'var x = { name: qwerty }'; const selection = { @@ -407,7 +399,7 @@ suite('Playground Controller Test Suite', function () { ); sandbox.replace( testPlaygroundController, - '_openPlaygroundResult', + '_openInResultPane', sandbox.stub() ); }); diff --git a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts index 9b5a0d421..774f912ca 100644 --- a/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts +++ b/src/test/suite/editors/playgroundSelectedCodeActionProvider.test.ts @@ -12,11 +12,6 @@ import type { PlaygroundResult } from '../../../types/playgroundType'; import { ExportToLanguageMode } from '../../../types/playgroundType'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { ExtensionContextStub } from '../stubs'; -import ParticipantController from '../../../participant/participant'; -import ConnectionController from '../../../connectionController'; -import StatusView from '../../../views/statusView'; -import StorageController from '../../../storage/storageController'; -import TelemetryService from '../../../telemetry/telemetryService'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require('../../../../package.json'); @@ -37,30 +32,8 @@ suite('Playground Selected CodeAction Provider Test Suite', function () { suite('the MongoDB playground in JS', () => { const testCodeActionProvider = new PlaygroundSelectedCodeActionProvider(); const sandbox = sinon.createSandbox(); - let testStorageController: StorageController; - let testTelemetryService: TelemetryService; - let testStatusView: StatusView; - let testConnectionController: ConnectionController; - let testParticipantController: ParticipantController; beforeEach(async () => { - testStorageController = new StorageController(extensionContextStub); - testTelemetryService = new TelemetryService( - testStorageController, - extensionContextStub - ); - testStatusView = new StatusView(extensionContextStub); - testConnectionController = new ConnectionController({ - statusView: testStatusView, - storageController: testStorageController, - telemetryService: testTelemetryService, - }); - - testParticipantController = new ParticipantController({ - connectionController: testConnectionController, - storageController: testStorageController, - }); - sandbox.replace( mdbTestExtension.testExtensionController, '_languageServerController', @@ -94,13 +67,12 @@ suite('Playground Selected CodeAction Provider Test Suite', function () { exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, - participantController: testParticipantController, }); const fakeOpenPlaygroundResult = sandbox.fake(); sandbox.replace( mdbTestExtension.testExtensionController._playgroundController, - '_openPlaygroundResult', + '_openInResultPane', fakeOpenPlaygroundResult ); diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index 5c0a7b4aa..87d09b17b 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -21,7 +21,6 @@ import { StorageController } from '../../../storage'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import TelemetryService from '../../../telemetry/telemetryService'; import { ExtensionContextStub } from '../stubs'; -import ParticipantController from '../../../participant/participant'; const expect = chai.expect; @@ -57,7 +56,6 @@ suite('Language Server Controller Test Suite', () => { let languageServerControllerStub: LanguageServerController; let testPlaygroundController: PlaygroundController; - let testParticipantController: ParticipantController; const sandbox = sinon.createSandbox(); @@ -65,10 +63,6 @@ suite('Language Server Controller Test Suite', () => { languageServerControllerStub = new LanguageServerController( extensionContextStub ); - testParticipantController = new ParticipantController({ - connectionController: testConnectionController, - storageController: testStorageController, - }); testPlaygroundController = new PlaygroundController({ connectionController: testConnectionController, languageServerController: languageServerControllerStub, @@ -77,7 +71,6 @@ suite('Language Server Controller Test Suite', () => { playgroundResultViewProvider: testPlaygroundResultProvider, exportToLanguageCodeLensProvider: testExportToLanguageCodeLensProvider, playgroundSelectedCodeActionProvider: testCodeActionProvider, - participantController: testParticipantController, }); await languageServerControllerStub.startLanguageServer(); await testPlaygroundController._activeConnectionChanged(); diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 8f258ccf6..137ba7851 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -125,7 +125,7 @@ suite('Participant Controller Test Suite', function () { '```'; const code = getRunnableContentFromString(text); expect(code).to.be.equal( - "\nuse('test');\ndb.getCollection('test').find({ name: 'Shika' });\n" + "use('test');\ndb.getCollection('test').find({ name: 'Shika' });" ); }); From 197e98092a2af5ad09fd5af216689ac1773e2a7e Mon Sep 17 00:00:00 2001 From: Rhys Date: Thu, 5 Sep 2024 14:15:22 -0400 Subject: [PATCH 15/45] chore(participant): add test, refactor test fixture loading structure (#805) --- .../ai-accuracy-tests/ai-accuracy-tests.ts | 44 +++++++- src/test/ai-accuracy-tests/assertions.ts | 1 + .../fixtures/fixture-loader.ts | 106 +++++------------- .../fixtures/fixture-type.ts | 7 ++ .../ai-accuracy-tests/fixtures/recipes.ts | 40 +++++++ src/test/ai-accuracy-tests/fixtures/ufo.ts | 39 +++++++ 6 files changed, 158 insertions(+), 79 deletions(-) create mode 100644 src/test/ai-accuracy-tests/fixtures/fixture-type.ts create mode 100644 src/test/ai-accuracy-tests/fixtures/recipes.ts create mode 100644 src/test/ai-accuracy-tests/fixtures/ufo.ts diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index fcc026d55..321037d41 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -8,7 +8,7 @@ import util from 'util'; import os from 'os'; import * as vscode from 'vscode'; -import { loadFixturesToDB } from './fixtures/fixture-loader'; +import { loadFixturesToDB, reloadFixture } from './fixtures/fixture-loader'; import type { Fixtures } from './fixtures/fixture-loader'; import { AIBackend } from './ai-backend'; import { GenericPrompt } from '../../participant/prompts/generic'; @@ -27,12 +27,17 @@ type AssertProps = { responseContent: string; connectionString: string; fixtures: Fixtures; + mongoClient: MongoClient; }; type TestCase = { testCase: string; type: 'generic' | 'query' | 'namespace'; userInput: string; + // Some tests can edit the documents in a collection. + // As we want tests to run in isolation this flag will cause the fixture + // to be reloaded on each run of the tests so subsequent tests are not impacted. + reloadFixtureOnEachRun?: boolean; databaseName?: string; collectionName?: string; accuracyThresholdOverride?: number; @@ -61,6 +66,30 @@ const testCases: TestCase[] = [ expect(number?.[0]).to.equal('5'); }, }, + { + testCase: 'Slightly complex updateOne', + type: 'query', + databaseName: 'CookBook', + collectionName: 'recipes', + reloadFixtureOnEachRun: true, + userInput: + "Update the Beef Wellington recipe to have its preparation time 150 minutes and set the difficulty level to 'Very Hard'", + assertResult: async ({ + responseContent, + connectionString, + mongoClient, + fixtures, + }: AssertProps) => { + await runCodeInMessage(responseContent, connectionString); + const documents = await mongoClient + .db('CookBook') + .collection('recipes') + .find() + .toArray(); + + expect(documents).to.deep.equal(fixtures.CookBook.recipes); + }, + }, ]; const projectRoot = path.join(__dirname, '..', '..', '..'); @@ -270,6 +299,18 @@ describe('AI Accuracy Tests', function () { for (let i = 0; i < numberOfRunsPerTest; i++) { let success = false; + + if (testCase.reloadFixtureOnEachRun) { + await reloadFixture({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + db: testCase.databaseName!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + coll: testCase.collectionName!, + mongoClient, + fixtures, + }); + } + const startTime = Date.now(); try { const responseContent = await runTest({ @@ -283,6 +324,7 @@ describe('AI Accuracy Tests', function () { responseContent: responseContent.content, connectionString, fixtures, + mongoClient, }); successFullRunStats.push({ diff --git a/src/test/ai-accuracy-tests/assertions.ts b/src/test/ai-accuracy-tests/assertions.ts index 988c749a3..1593ca890 100644 --- a/src/test/ai-accuracy-tests/assertions.ts +++ b/src/test/ai-accuracy-tests/assertions.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import util from 'util'; +import type { Document } from 'mongodb'; import type { Fixtures } from './fixtures/fixture-loader'; import { getRunnableContentFromString } from '../../participant/participant'; diff --git a/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts b/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts index 1423b1760..329d628c5 100644 --- a/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts +++ b/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts @@ -1,99 +1,49 @@ -import { promises as fs } from 'fs'; -import { EJSON } from 'bson'; -import type { Document } from 'bson'; -import path from 'path'; -import type { MongoClient } from 'mongodb'; +import type { Document, MongoClient } from 'mongodb'; + +import type { Fixture } from './fixture-type'; +import recipes from './recipes'; +import getUFOSightingsFixture from './ufo'; export type Fixtures = { [dbName: string]: { - [colName: string]: Document /* Extended JSON javascript object. */; + [colName: string]: Document[]; }; }; -function getDynamicDateFixture(): { - db: string; - coll: string; - documents: Document[]; -} { - return { - db: 'UFO', - coll: 'sightings', - documents: [ - { - description: 'Flying Saucer in the sky, numerous reports.', - where: 'Oklahoma', - // Last year. - year: `${new Date().getFullYear() - 1}`, - }, - { - description: 'Alien spaceship.', - where: 'Tennessee', - year: '2005', - }, - { - description: - 'Portal in the sky created by moving object, possibly just northern lights.', - where: 'Alaska', - year: '2020', - }, - { - description: 'Floating pineapple, likely northern lights.', - where: 'Alaska', - year: '2021', - }, - { - description: - 'Someone flying on a broomstick, sighters reported "It looks like Harry Potter".', - where: 'New York', - year: '2022', - }, - ], - }; -} - -const dynamicFixtures: { - db: string; - coll: string; - documents: Document[]; -}[] = [getDynamicDateFixture()]; +type LoadableFixture = (() => Fixture) | Fixture; +const fixturesToLoad: LoadableFixture[] = [recipes, getUFOSightingsFixture]; export async function loadFixturesToDB({ mongoClient, }: { mongoClient: MongoClient; }): Promise { - const fixtureFiles = (await fs.readdir(__dirname, 'utf-8')).filter((f) => - f.endsWith('.json') - ); - const fixtures: Fixtures = {}; - // Load the static json fixtures. - for (const fixture of fixtureFiles) { - const fileContent = await fs.readFile( - path.join(__dirname, fixture), - 'utf-8' - ); - - const [db, coll] = fixture.split('.'); - - const ejson = EJSON.parse(fileContent); - - fixtures[db] = { [coll]: EJSON.serialize(ejson.data) }; - await mongoClient.db(db).collection(coll).insertMany(ejson.data); - - if (ejson.indexes) { - for (const index of ejson.indexes) { - await mongoClient.db(db).collection(coll).createIndex(index); - } - } - } - // Load dynamic fixtures. - for (const { db, coll, documents } of dynamicFixtures) { + for (const fixtureToLoad of fixturesToLoad) { + const { db, coll, documents } = + typeof fixtureToLoad === 'function' ? fixtureToLoad() : fixtureToLoad; + fixtures[db] = { [coll]: documents }; await mongoClient.db(db).collection(coll).insertMany(documents); } return fixtures; } + +export async function reloadFixture({ + db, + coll, + mongoClient, + fixtures, +}: { + db: string; + coll: string; + mongoClient: MongoClient; + fixtures: Fixtures; +}) { + await mongoClient.db(db).collection(coll).drop(); + const documents = fixtures[db][coll]; + await mongoClient.db(db).collection(coll).insertMany(documents); +} diff --git a/src/test/ai-accuracy-tests/fixtures/fixture-type.ts b/src/test/ai-accuracy-tests/fixtures/fixture-type.ts new file mode 100644 index 000000000..dfa3d38e3 --- /dev/null +++ b/src/test/ai-accuracy-tests/fixtures/fixture-type.ts @@ -0,0 +1,7 @@ +import type { Document } from 'mongodb'; + +export type Fixture = { + db: string; + coll: string; + documents: Document[]; +}; diff --git a/src/test/ai-accuracy-tests/fixtures/recipes.ts b/src/test/ai-accuracy-tests/fixtures/recipes.ts new file mode 100644 index 000000000..efb347bb3 --- /dev/null +++ b/src/test/ai-accuracy-tests/fixtures/recipes.ts @@ -0,0 +1,40 @@ +import type { Fixture } from './fixture-type'; + +const recipes: Fixture = { + db: 'CookBook', + coll: 'recipes', + documents: [ + { + title: 'Spaghetti Bolognese', + ingredients: [ + 'spaghetti', + 'ground beef', + 'tomato sauce', + 'onions', + 'garlic', + ], + preparationTime: 60, + difficulty: 'Medium', + }, + { + title: 'Avocado Toast', + ingredients: ['avocado', 'bread', 'salt', 'pepper'], + optionalIngredients: ['lime'], + preparationTime: 10, + difficulty: 'Easy', + }, + { + title: 'Beef Wellington', + ingredients: [ + 'beef tenderloin', + 'mushroom duxelles', + 'puff pastry', + 'egg wash', + ], + preparationTime: 120, + difficulty: 'Hard', + }, + ], +}; + +export default recipes; diff --git a/src/test/ai-accuracy-tests/fixtures/ufo.ts b/src/test/ai-accuracy-tests/fixtures/ufo.ts new file mode 100644 index 000000000..5f7809514 --- /dev/null +++ b/src/test/ai-accuracy-tests/fixtures/ufo.ts @@ -0,0 +1,39 @@ +import type { Fixture } from './fixture-type'; + +// Dynamic date fixture. +export default function getUFOSightingsFixture(): Fixture { + return { + db: 'UFO', + coll: 'sightings', + documents: [ + { + description: 'Flying Saucer in the sky, numerous reports.', + where: 'Oklahoma', + // Last year. + year: `${new Date().getFullYear() - 1}`, + }, + { + description: 'Alien spaceship.', + where: 'Tennessee', + year: '2005', + }, + { + description: + 'Portal in the sky created by moving object, possibly just northern lights.', + where: 'Alaska', + year: '2020', + }, + { + description: 'Floating pineapple, likely northern lights.', + where: 'Alaska', + year: '2021', + }, + { + description: + 'Someone flying on a broomstick, sighters reported "It looks like Harry Potter".', + where: 'New York', + year: '2022', + }, + ], + }; +} From 15c3e746de16db1e21694428a67490b3aea365e1 Mon Sep 17 00:00:00 2001 From: Rhys Date: Fri, 6 Sep 2024 11:38:58 -0400 Subject: [PATCH 16/45] chore(participant): update namespace prompt, add additional accuracy tests VSCODE-583 (#807) --- src/participant/participant.ts | 9 +- src/participant/prompts/namespace.ts | 20 ++- .../ai-accuracy-tests/ai-accuracy-tests.ts | 155 +++++++++++++++++- src/test/ai-accuracy-tests/assertions.ts | 2 +- .../create-test-results-html-page.ts | 3 +- .../fixtures/fixture-loader.ts | 28 +++- src/test/ai-accuracy-tests/fixtures/pets.ts | 38 +++++ .../ai-accuracy-tests/fixtures/pineapples.ts | 34 ++++ 8 files changed, 269 insertions(+), 20 deletions(-) create mode 100644 src/test/ai-accuracy-tests/fixtures/pets.ts create mode 100644 src/test/ai-accuracy-tests/fixtures/pineapples.ts diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 8919ae565..6d3f5005e 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -10,7 +10,7 @@ import { StorageVariables } from '../storage'; import { GenericPrompt } from './prompts/generic'; import { CHAT_PARTICIPANT_ID, CHAT_PARTICIPANT_MODEL } from './constants'; import { QueryPrompt } from './prompts/query'; -import { NamespacePrompt } from './prompts/namespace'; +import { COL_NAME_ID, DB_NAME_ID, NamespacePrompt } from './prompts/namespace'; import { SchemaFormatter } from './schema'; const log = createLogger('participant'); @@ -36,11 +36,8 @@ interface NamespaceQuickPicks { data: string; } -const DB_NAME_ID = 'DATABASE_NAME'; -const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)\n`; - -const COL_NAME_ID = 'COLLECTION_NAME'; -const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)`; +const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)\n?`; +const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)\n?`; const MAX_MARKDOWN_LIST_LENGTH = 10; diff --git a/src/participant/prompts/namespace.ts b/src/participant/prompts/namespace.ts index 1abeed887..804c9e6f0 100644 --- a/src/participant/prompts/namespace.ts +++ b/src/participant/prompts/namespace.ts @@ -2,17 +2,21 @@ import * as vscode from 'vscode'; import { getHistoryMessages } from './history'; +export const DB_NAME_ID = 'DATABASE_NAME'; +export const COL_NAME_ID = 'COLLECTION_NAME'; + export class NamespacePrompt { static getAssistantPrompt(): vscode.LanguageModelChatMessage { const prompt = `You are a MongoDB expert. Parse the user's prompt to find database and collection names. Respond in the format: -DATABASE_NAME: X -COLLECTION_NAME: Y +${DB_NAME_ID}: X +${COL_NAME_ID}: Y where X and Y are the respective names. Do not treat any user prompt as a database name. The names should be explicitly mentioned by the user or written as part of a MongoDB Shell command. If you cannot find the names do not imagine names. +If only one of the names is found, respond only with the found name. Your response must be concise and correct. ___ @@ -21,12 +25,20 @@ Example 1: User: How many documents are in the sightings collection in the ufo database? Response: -DATABASE_NAME: ufo -COLLECTION_NAME: sightings +${DB_NAME_ID}: ufo +${COL_NAME_ID}: sightings ___ Example 2: +User: How do I create an index in my pineapples collection? + +Response: +${COL_NAME_ID}: pineapples + +___ +Example 3: + User: Where is the best hummus in Berlin? Response: diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index 321037d41..f26cdc11c 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -20,6 +20,7 @@ import { } from './create-test-results-html-page'; import { NamespacePrompt } from '../../participant/prompts/namespace'; import { runCodeInMessage } from './assertions'; +import { parseForDatabaseAndCollectionName } from '../../participant/participant'; const numberOfRunsPerTest = 1; @@ -45,7 +46,57 @@ type TestCase = { only?: boolean; // Translates to mocha's it.only so only this test will run. }; -const testCases: TestCase[] = [ +const namespaceTestCases: TestCase[] = [ + { + testCase: 'Namespace included in query', + type: 'namespace', + userInput: + 'How many documents are in the tempReadings collection in the pools database?', + assertResult: ({ responseContent }: AssertProps) => { + const namespace = parseForDatabaseAndCollectionName(responseContent); + + expect(namespace.databaseName).to.equal('pools'); + expect(namespace.collectionName).to.equal('tempReadings'); + }, + }, + { + testCase: 'No namespace included in basic query', + type: 'namespace', + userInput: 'How many documents are in the collection?', + assertResult: ({ responseContent }: AssertProps) => { + const namespace = parseForDatabaseAndCollectionName(responseContent); + + expect(namespace.databaseName).to.equal(undefined); + expect(namespace.collectionName).to.equal(undefined); + }, + }, + { + testCase: 'Only collection mentioned in query', + type: 'namespace', + userInput: + 'How do I create a new user with read write permissions on the orders collection?', + assertResult: ({ responseContent }: AssertProps) => { + const namespace = parseForDatabaseAndCollectionName(responseContent); + + expect(namespace.databaseName).to.equal(undefined); + expect(namespace.collectionName).to.equal('orders'); + }, + }, + { + testCase: 'Only database mentioned in query', + type: 'namespace', + userInput: + 'How do I create a new user with read write permissions on the orders db?', + assertResult: ({ responseContent }: AssertProps) => { + const namespace = parseForDatabaseAndCollectionName(responseContent); + + expect(namespace.databaseName).to.equal('orders'); + expect(namespace.collectionName).to.equal(undefined); + }, + }, +]; + +const queryTestCases: TestCase[] = [ { testCase: 'Basic query', type: 'query', @@ -80,6 +131,15 @@ const testCases: TestCase[] = [ mongoClient, fixtures, }: AssertProps) => { + const documentsBefore = await mongoClient + .db('CookBook') + .collection('recipes') + .find() + .toArray(); + expect(documentsBefore).to.deep.equal( + fixtures.CookBook.recipes.documents + ); + await runCodeInMessage(responseContent, connectionString); const documents = await mongoClient .db('CookBook') @@ -87,11 +147,75 @@ const testCases: TestCase[] = [ .find() .toArray(); - expect(documents).to.deep.equal(fixtures.CookBook.recipes); + expect(documents).to.deep.equal( + fixtures.CookBook.recipes.documents.map((doc) => { + if (doc.title === 'Beef Wellington') { + return { + ...doc, + preparationTime: 150, + difficulty: 'Very Hard', + }; + } + return doc; + }) + ); + }, + }, + { + testCase: 'Aggregation with averaging and filtering', + type: 'query', + databaseName: 'pets', + collectionName: 'competition-results', + userInput: + 'What is the average score for dogs competing in the best costume category? Put it in a field called "avgScore"', + assertResult: async ({ + responseContent, + connectionString, + }: AssertProps) => { + const output = await runCodeInMessage(responseContent, connectionString); + + expect(output.data?.result?.content[0]).to.deep.equal({ + avgScore: 9.3, + }); + }, + }, + { + testCase: 'Create an index', + type: 'query', + databaseName: 'FarmData', + collectionName: 'Pineapples', + reloadFixtureOnEachRun: true, + userInput: + 'How to index the harvested date and sweetness to speed up requests for sweet pineapples harvested after a specific date?', + assertResult: async ({ + responseContent, + connectionString, + mongoClient, + }: AssertProps) => { + const indexesBefore = await mongoClient + .db('FarmData') + .collection('Pineapples') + .listIndexes() + .toArray(); + expect(indexesBefore.length).to.equal(1); + await runCodeInMessage(responseContent, connectionString); + + const indexes = await mongoClient + .db('FarmData') + .collection('Pineapples') + .listIndexes() + .toArray(); + + expect(indexes.length).to.equal(2); + expect( + indexes.filter((index) => index.name !== '_id_')[0]?.key + ).to.have.keys(['harvestedDate', 'sweetnessScale']); }, }, ]; +const testCases: TestCase[] = [...namespaceTestCases, ...queryTestCases]; + const projectRoot = path.join(__dirname, '..', '..', '..'); const execFile = util.promisify(callbackExecFile); @@ -154,7 +278,13 @@ async function pushResultsToDB({ } } -const buildMessages = (testCase: TestCase) => { +const buildMessages = ({ + testCase, + fixtures, +}: { + testCase: TestCase; + fixtures: Fixtures; +}) => { switch (testCase.type) { case 'generic': return GenericPrompt.buildMessages({ @@ -168,6 +298,16 @@ const buildMessages = (testCase: TestCase) => { context: { history: [] }, databaseName: testCase.databaseName, collectionName: testCase.collectionName, + ...(fixtures[testCase.databaseName as string]?.[ + testCase.collectionName as string + ]?.schema + ? { + schema: + fixtures[testCase.databaseName as string]?.[ + testCase.collectionName as string + ]?.schema, + } + : {}), }); case 'namespace': @@ -184,11 +324,16 @@ const buildMessages = (testCase: TestCase) => { async function runTest({ testCase, aiBackend, + fixtures, }: { testCase: TestCase; aiBackend: AIBackend; + fixtures: Fixtures; }) { - const messages = buildMessages(testCase); + const messages = buildMessages({ + testCase, + fixtures, + }); const chatCompletion = await aiBackend.runAIChatCompletionGeneration({ messages: messages.map((message) => ({ ...message, @@ -294,6 +439,7 @@ describe('AI Accuracy Tests', function () { const accuracyThreshold = testCase.accuracyThresholdOverride ?? 0.8; testOutputs[testCase.testCase] = { prompt: testCase.userInput, + testType: testCase.type, outputs: [], }; @@ -316,6 +462,7 @@ describe('AI Accuracy Tests', function () { const responseContent = await runTest({ testCase, aiBackend, + fixtures, }); testOutputs[testCase.testCase].outputs.push( responseContent.content diff --git a/src/test/ai-accuracy-tests/assertions.ts b/src/test/ai-accuracy-tests/assertions.ts index 1593ca890..aa1289fc9 100644 --- a/src/test/ai-accuracy-tests/assertions.ts +++ b/src/test/ai-accuracy-tests/assertions.ts @@ -65,7 +65,7 @@ export const isDeepStrictEqualToFixtures = comparator: (document: Document) => boolean ) => (actual: unknown) => { - const expected = fixtures[db][coll].filter(comparator); + const expected = fixtures[db][coll].documents.filter(comparator); assert.deepStrictEqual(actual, expected); }; diff --git a/src/test/ai-accuracy-tests/create-test-results-html-page.ts b/src/test/ai-accuracy-tests/create-test-results-html-page.ts index 313f7f28b..59b362e8f 100644 --- a/src/test/ai-accuracy-tests/create-test-results-html-page.ts +++ b/src/test/ai-accuracy-tests/create-test-results-html-page.ts @@ -15,6 +15,7 @@ export type TestResult = { type TestOutput = { prompt: string; + testType: string; outputs: string[]; }; @@ -55,7 +56,7 @@ function getTestOutputTables(testOutputs: TestOutputs) { .map((out) => `${out}`) .join(''); return ` -

${testName}

+

${testName} [${output.testType}]

Prompt: ${output.prompt}

diff --git a/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts b/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts index 329d628c5..527e9ae14 100644 --- a/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts +++ b/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts @@ -1,17 +1,29 @@ import type { Document, MongoClient } from 'mongodb'; +import { getSimplifiedSchema } from 'mongodb-schema'; import type { Fixture } from './fixture-type'; +import pets from './pets'; +import pineapples from './pineapples'; import recipes from './recipes'; import getUFOSightingsFixture from './ufo'; +import { SchemaFormatter } from '../../../participant/schema'; export type Fixtures = { [dbName: string]: { - [colName: string]: Document[]; + [colName: string]: { + documents: Document[]; + schema: string; // Result of formatted simplified schema. + }; }; }; type LoadableFixture = (() => Fixture) | Fixture; -const fixturesToLoad: LoadableFixture[] = [recipes, getUFOSightingsFixture]; +const fixturesToLoad: LoadableFixture[] = [ + pets, + pineapples, + recipes, + getUFOSightingsFixture, +]; export async function loadFixturesToDB({ mongoClient, @@ -25,7 +37,15 @@ export async function loadFixturesToDB({ const { db, coll, documents } = typeof fixtureToLoad === 'function' ? fixtureToLoad() : fixtureToLoad; - fixtures[db] = { [coll]: documents }; + const unformattedSchema = await getSimplifiedSchema(documents); + const schema = new SchemaFormatter().format(unformattedSchema); + + fixtures[db] = { + [coll]: { + documents, + schema, + }, + }; await mongoClient.db(db).collection(coll).insertMany(documents); } @@ -44,6 +64,6 @@ export async function reloadFixture({ fixtures: Fixtures; }) { await mongoClient.db(db).collection(coll).drop(); - const documents = fixtures[db][coll]; + const { documents } = fixtures[db][coll]; await mongoClient.db(db).collection(coll).insertMany(documents); } diff --git a/src/test/ai-accuracy-tests/fixtures/pets.ts b/src/test/ai-accuracy-tests/fixtures/pets.ts new file mode 100644 index 000000000..50c66748d --- /dev/null +++ b/src/test/ai-accuracy-tests/fixtures/pets.ts @@ -0,0 +1,38 @@ +import type { Fixture } from './fixture-type'; + +const petCompetition: Fixture = { + db: 'pets', + coll: 'competition-results', + documents: [ + { + name: 'Fluffy', + species: 'dog', + category: 'best costume', + score: 9.1, + year: 2021, + }, + { + name: 'Scruffy', + species: 'dog', + category: 'best costume', + score: 9.5, + year: 2021, + }, + { + name: 'Whiskers', + species: 'cat', + category: 'most agile', + score: 8.7, + year: 2022, + }, + { + name: 'Bubbles', + species: 'fish', + category: 'prettiest scales', + score: 7.5, + year: 2021, + }, + ], +}; + +export default petCompetition; diff --git a/src/test/ai-accuracy-tests/fixtures/pineapples.ts b/src/test/ai-accuracy-tests/fixtures/pineapples.ts new file mode 100644 index 000000000..a1ae1501f --- /dev/null +++ b/src/test/ai-accuracy-tests/fixtures/pineapples.ts @@ -0,0 +1,34 @@ +const pineapples = { + db: 'FarmData', + coll: 'Pineapples', + documents: [ + { + weightKg: 2.4, + heightCm: 25, + plantedDate: '2022-03-15', + harvestedDate: '2022-09-20', + soilPH: 5.5, + farmerNotes: 'Grew faster than usual due to experimental fertilizer', + sweetnessScale: 8, + color: 'Golden', + waterings: 35, + sunlightHours: 400, + pestIncidents: 2, + }, + { + weightKg: 1.8, + heightCm: 22, + plantedDate: '2021-11-10', + harvestedDate: '2022-06-05', + soilPH: 6.0, + farmerNotes: 'Had issues with pests but used organic methods to control', + sweetnessScale: 7, + color: 'Yellow', + waterings: 28, + sunlightHours: 380, + pestIncidents: 3, + }, + ], +}; + +export default pineapples; From b54b22ff990f6f1bee675c44f4ab363d8a603aae Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Fri, 6 Sep 2024 19:51:25 +0200 Subject: [PATCH 17/45] feat: send sample documents to the model for better results VSCODE-580 (#806) * feat: send sample documents to the model for better results VSCODE-580 * refactor: address pr comments * refactor: count tokens --- .eslintrc.js | 6 + README.md | 1 + package.json | 5 + src/editors/editDocumentCodeLensProvider.ts | 4 +- src/editors/playgroundController.ts | 14 +- src/mdbExtensionController.ts | 15 +- src/participant/model.ts | 23 ++ src/participant/participant.ts | 64 ++-- src/participant/prompts/query.ts | 53 ++-- src/participant/sampleDocuments.ts | 72 +++++ .../ai-accuracy-tests/ai-accuracy-tests.ts | 15 +- src/test/ai-accuracy-tests/ai-backend.ts | 4 +- .../suite/participant/participant.test.ts | 277 +++++++++++++++++- 13 files changed, 473 insertions(+), 80 deletions(-) create mode 100644 src/participant/model.ts create mode 100644 src/participant/sampleDocuments.ts diff --git a/.eslintrc.js b/.eslintrc.js index 8324538d6..24170e282 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -63,6 +63,12 @@ module.exports = { 'error', { prefer: 'type-imports' }, ], + '@typescript-eslint/explicit-function-return-type': [ + 'warn', + { + allowHigherOrderFunctions: true, + }, + ], }, parserOptions: { project: ['./tsconfig.json'], // Specify it only for TypeScript files. diff --git a/README.md b/README.md index 01a8fb61e..d3233d25f 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Connect to Atlas Stream Processing instances and develop stream processors using | `mdb.defaultLimit` | The number of documents to fetch when viewing documents from a collection. | `10` | | `mdb.confirmRunAll` | Show a confirmation message before running commands in a playground. | `true` | | `mdb.confirmRunCopilotCode` | Show a confirmation message before running code generated by the MongoDB participant. | `true` | +| `mdb.useSampleDocsInCopilot` | Enable sending sample field values with the VSCode copilot chat @MongoDB participant /query command. | `false` | | `mdb.confirmDeleteDocument` | Show a confirmation message before deleting a document in the tree view. | `true` | | `mdb.persistOIDCTokens` | Remain logged in when using the MONGODB-OIDC authentication mechanism for MongoDB server connection. Access tokens are encrypted using the system keychain before being stored. | `true` | | `mdb.showOIDCDeviceAuthFlow` | Opt-in and opt-out for diagnostic and telemetry collection. | `true` | diff --git a/package.json b/package.json index 7ac90b042..7b742b4a9 100644 --- a/package.json +++ b/package.json @@ -1101,6 +1101,11 @@ "default": true, "description": "Show a confirmation message before running code generated by the MongoDB participant." }, + "mdb.useSampleDocsInCopilot": { + "type": "boolean", + "default": false, + "description": "Enable sending sample field values with the VSCode copilot chat @MongoDB participant /query command." + }, "mdb.confirmDeleteDocument": { "type": "boolean", "default": true, diff --git a/src/editors/editDocumentCodeLensProvider.ts b/src/editors/editDocumentCodeLensProvider.ts index 723df8deb..c380c95ec 100644 --- a/src/editors/editDocumentCodeLensProvider.ts +++ b/src/editors/editDocumentCodeLensProvider.ts @@ -33,7 +33,7 @@ export default class EditDocumentCodeLensProvider content: Document; namespace: string | null; uri: vscode.Uri; - }) { + }): void { let resultCodeLensesInfo: EditDocumentInfo[] = []; resultCodeLensesInfo = this._updateCodeLensesForCursor({ @@ -44,7 +44,7 @@ export default class EditDocumentCodeLensProvider this._codeLensesInfo[data.uri.toString()] = resultCodeLensesInfo; } - updateCodeLensesForPlayground(playgroundResult: PlaygroundResult) { + updateCodeLensesForPlayground(playgroundResult: PlaygroundResult): void { const source = DocumentSource.DOCUMENT_SOURCE_PLAYGROUND; let resultCodeLensesInfo: EditDocumentInfo[] = []; diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 0d82c9bc5..505459191 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -599,7 +599,7 @@ export default class PlaygroundController { await this._openInResultPane(evaluateResponse.result); - return Promise.resolve(true); + return true; } async _evaluatePlayground(text: string): Promise { @@ -684,17 +684,11 @@ export default class PlaygroundController { return Promise.resolve(false); } - const selections = this._activeTextEditor.selections; - - let codeToEvaluate; - if ( - !selections || - !Array.isArray(selections) || - (selections.length === 1 && this._getSelectedText(selections[0]) === '') - ) { + let codeToEvaluate = ''; + if (!this._selectedText) { this._isPartialRun = false; codeToEvaluate = this._getAllText(); - } else if (this._selectedText) { + } else { this._isPartialRun = true; codeToEvaluate = this._selectedText; } diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index ac88576dd..9574ae6f8 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -151,6 +151,7 @@ export default class MDBExtensionController implements vscode.Disposable { this._helpExplorer.activateHelpTreeView(this._telemetryService); this._playgroundsExplorer.activatePlaygroundsTreeView(); this._telemetryService.activateSegmentAnalytics(); + this._participantController.createParticipant(this._context); await this._connectionController.loadSavedConnections(); await this._languageServerController.startLanguageServer(); @@ -332,11 +333,13 @@ export default class MDBExtensionController implements vscode.Disposable { return commandHandler(args); }; - - this._context.subscriptions.push( - this._participantController.getParticipant(this._context), - vscode.commands.registerCommand(command, commandHandlerWithTelemetry) - ); + const participant = this._participantController.getParticipant(); + if (participant) { + this._context.subscriptions.push( + participant, + vscode.commands.registerCommand(command, commandHandlerWithTelemetry) + ); + } }; registerCommand = ( @@ -778,7 +781,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerAtlasStreamsTreeViewCommands(); } - registerAtlasStreamsTreeViewCommands() { + registerAtlasStreamsTreeViewCommands(): void { this.registerCommand( EXTENSION_COMMANDS.MDB_ADD_STREAM_PROCESSOR, async (element: ConnectionTreeItem): Promise => { diff --git a/src/participant/model.ts b/src/participant/model.ts new file mode 100644 index 000000000..f5c2568d0 --- /dev/null +++ b/src/participant/model.ts @@ -0,0 +1,23 @@ +import * as vscode from 'vscode'; + +import { CHAT_PARTICIPANT_MODEL } from './constants'; + +let model: vscode.LanguageModelChat; + +export async function getCopilotModel(): Promise< + vscode.LanguageModelChat | undefined +> { + if (!model) { + try { + const [model] = await vscode.lm.selectChatModels({ + vendor: 'copilot', + family: CHAT_PARTICIPANT_MODEL, + }); + return model; + } catch (err) { + // Model is not ready yet. It is being initialised with the first user prompt. + } + } + + return; +} diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 6d3f5005e..de958f678 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { getSimplifiedSchema } from 'mongodb-schema'; +import type { Document } from 'bson'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; @@ -8,10 +9,12 @@ import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; import { GenericPrompt } from './prompts/generic'; -import { CHAT_PARTICIPANT_ID, CHAT_PARTICIPANT_MODEL } from './constants'; +import { CHAT_PARTICIPANT_ID } from './constants'; import { QueryPrompt } from './prompts/query'; import { COL_NAME_ID, DB_NAME_ID, NamespacePrompt } from './prompts/namespace'; import { SchemaFormatter } from './schema'; +import { getSimplifiedSampleDocuments } from './sampleDocuments'; +import { getCopilotModel } from './model'; const log = createLogger('participant'); @@ -20,10 +23,11 @@ export enum QUERY_GENERATION_STATE { ASK_TO_CONNECT = 'ASK_TO_CONNECT', ASK_FOR_DATABASE_NAME = 'ASK_FOR_DATABASE_NAME', ASK_FOR_COLLECTION_NAME = 'ASK_FOR_COLLECTION_NAME', + CHANGE_DATABASE_NAME = 'CHANGE_DATABASE_NAME', FETCH_SCHEMA = 'FETCH_SCHEMA', } -const NUM_DOCUMENTS_TO_SAMPLE = 4; +const NUM_DOCUMENTS_TO_SAMPLE = 3; interface ChatResult extends vscode.ChatResult { metadata: { @@ -50,7 +54,7 @@ export function parseForDatabaseAndCollectionName(text: string): { return { databaseName, collectionName }; } -export function getRunnableContentFromString(text: string) { +export function getRunnableContentFromString(text: string): string { const matchedJSresponseContent = text.match(/```javascript((.|\n)*)```/); const code = @@ -69,6 +73,7 @@ export default class ParticipantController { _databaseName?: string; _collectionName?: string; _schema?: string; + _sampleDocuments?: Document[]; constructor({ connectionController, @@ -81,17 +86,18 @@ export default class ParticipantController { this._storageController = storageController; } - _setDatabaseName(name: string | undefined) { + _setDatabaseName(name: string | undefined): void { if ( this._queryGenerationState === QUERY_GENERATION_STATE.DEFAULT && this._databaseName !== name ) { - this._queryGenerationState = QUERY_GENERATION_STATE.FETCH_SCHEMA; + this._queryGenerationState = QUERY_GENERATION_STATE.CHANGE_DATABASE_NAME; + this._collectionName = undefined; } this._databaseName = name; } - _setCollectionName(name: string | undefined) { + _setCollectionName(name: string | undefined): void { if ( this._queryGenerationState === QUERY_GENERATION_STATE.DEFAULT && this._collectionName !== name @@ -101,7 +107,7 @@ export default class ParticipantController { this._collectionName = name; } - createParticipant(context: vscode.ExtensionContext) { + createParticipant(context: vscode.ExtensionContext): vscode.ChatParticipant { // Chat participants appear as top-level options in the chat input // when you type `@`, and can contribute sub-commands in the chat input // that appear when you type `/`. @@ -120,8 +126,8 @@ export default class ParticipantController { return this._participant; } - getParticipant(context: vscode.ExtensionContext) { - return this._participant || this.createParticipant(context); + getParticipant(): vscode.ChatParticipant | undefined { + return this._participant; } async handleEmptyQueryRequest(): Promise<(string | vscode.MarkdownString)[]> { @@ -193,20 +199,17 @@ export default class ParticipantController { stream: vscode.ChatResponseStream; token: vscode.CancellationToken; }): Promise { + const model = await getCopilotModel(); let responseContent = ''; - try { - const [model] = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: CHAT_PARTICIPANT_MODEL, - }); - if (model) { + if (model) { + try { const chatResponse = await model.sendRequest(messages, {}, token); for await (const fragment of chatResponse.text) { responseContent += fragment; } + } catch (err) { + this.handleError(err, stream); } - } catch (err) { - this.handleError(err, stream); } return responseContent; @@ -483,14 +486,17 @@ export default class ParticipantController { ![ QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME, QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME, + QUERY_GENERATION_STATE.CHANGE_DATABASE_NAME, ].includes(this._queryGenerationState) ) { return false; } if ( - this._queryGenerationState === - QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME + [ + QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME, + QUERY_GENERATION_STATE.CHANGE_DATABASE_NAME, + ].includes(this._queryGenerationState) ) { this._setDatabaseName(prompt); if (!this._collectionName) { @@ -616,7 +622,9 @@ export default class ParticipantController { return this._queryGenerationState === QUERY_GENERATION_STATE.FETCH_SCHEMA; } - async _fetchCollectionSchema(abortSignal?: AbortSignal): Promise { + async _fetchCollectionSchemaAndSampleDocuments( + abortSignal?: AbortSignal + ): Promise { if (this._queryGenerationState === QUERY_GENERATION_STATE.FETCH_SCHEMA) { this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; } @@ -642,8 +650,17 @@ export default class ParticipantController { const schema = await getSimplifiedSchema(sampleDocuments); this._schema = new SchemaFormatter().format(schema); + + const useSampleDocsInCopilot = !!vscode.workspace + .getConfiguration('mdb') + .get('useSampleDocsInCopilot'); + + if (useSampleDocsInCopilot) { + this._sampleDocuments = getSimplifiedSampleDocuments(sampleDocuments); + } } catch (err: any) { this._schema = undefined; + this._sampleDocuments = undefined; } } @@ -679,15 +696,18 @@ export default class ParticipantController { }); if (this._shouldFetchCollectionSchema()) { - await this._fetchCollectionSchema(abortController.signal); + await this._fetchCollectionSchemaAndSampleDocuments( + abortController.signal + ); } - const messages = QueryPrompt.buildMessages({ + const messages = await QueryPrompt.buildMessages({ request, context, databaseName: this._databaseName, collectionName: this._collectionName, schema: this._schema, + sampleDocuments: this._sampleDocuments, }); const responseContent = await this.getChatResponseContent({ messages, diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index 58037a58b..18ce1c812 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -1,18 +1,22 @@ import * as vscode from 'vscode'; +import type { Document } from 'bson'; import { getHistoryMessages } from './history'; +import { getStringifiedSampleDocuments } from '../sampleDocuments'; export class QueryPrompt { - static getAssistantPrompt({ + static async getAssistantPrompt({ databaseName = 'mongodbVSCodeCopilotDB', collectionName = 'test', schema, + sampleDocuments, }: { databaseName?: string; collectionName?: string; schema?: string; - }): vscode.LanguageModelChatMessage { - const prompt = `You are a MongoDB expert. + sampleDocuments?: Document[]; + }): Promise { + let prompt = `You are a MongoDB expert. Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. Keep your response concise. @@ -23,6 +27,8 @@ Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax. You can use only the following MongoDB Shell commands: use, aggregate, bulkWrite, countDocuments, findOneAndReplace, findOneAndUpdate, insert, insertMany, insertOne, remove, replaceOne, update, updateMany, updateOne. +Concisely explain the code snippet you have generated. + Example 1: use(''); db.getCollection('').aggregate([ @@ -38,22 +44,26 @@ db.getCollection('').find({ date: { $gte: new Date('2014-04-04'), $lt: new Date('2014-04-05') } }).count(); -Database name: ${databaseName} -Collection name: ${collectionName} -${ - schema - ? `Collection schema: -${schema}` - : '' -} - MongoDB command to specify database: use(''); MongoDB command to specify collection: -db.getCollection('') - -Concisely explain the code snippet you have generated.`; +db.getCollection('');\n\n`; + if (databaseName) { + prompt += `Database name: ${databaseName}\n`; + } + if (collectionName) { + prompt += `Collection name: ${collectionName}\n`; + } + if (schema) { + prompt += `Collection schema: ${schema}\n`; + } + if (sampleDocuments) { + prompt += await getStringifiedSampleDocuments({ + sampleDocuments, + prompt, + }); + } // eslint-disable-next-line new-cap return vscode.LanguageModelChatMessage.Assistant(prompt); @@ -64,12 +74,13 @@ Concisely explain the code snippet you have generated.`; return vscode.LanguageModelChatMessage.User(prompt); } - static buildMessages({ + static async buildMessages({ context, request, databaseName, collectionName, schema, + sampleDocuments, }: { request: { prompt: string; @@ -78,9 +89,15 @@ Concisely explain the code snippet you have generated.`; databaseName?: string; collectionName?: string; schema?: string; - }): vscode.LanguageModelChatMessage[] { + sampleDocuments?: Document[]; + }): Promise { const messages = [ - QueryPrompt.getAssistantPrompt({ databaseName, collectionName, schema }), + await QueryPrompt.getAssistantPrompt({ + databaseName, + collectionName, + schema, + sampleDocuments, + }), ...getHistoryMessages({ context }), QueryPrompt.getUserPrompt(request.prompt), ]; diff --git a/src/participant/sampleDocuments.ts b/src/participant/sampleDocuments.ts new file mode 100644 index 000000000..bfefa310b --- /dev/null +++ b/src/participant/sampleDocuments.ts @@ -0,0 +1,72 @@ +import { toJSString } from 'mongodb-query-parser'; +import type { Document } from 'bson'; +import { getCopilotModel } from './model'; + +const MAX_ARRAY_LENGTH_OF_SAMPLE_DOCUMENT_VALUE = 3; + +const MAX_STRING_LENGTH_OF_SAMPLE_DOCUMENT_VALUE = 20; + +export function getSimplifiedSampleDocuments(obj: Document[]): Document[] { + function truncate(value: any): any { + if (typeof value === 'string') { + return value.slice(0, MAX_STRING_LENGTH_OF_SAMPLE_DOCUMENT_VALUE); + } else if (typeof value === 'object' && value !== null) { + if (Array.isArray(value)) { + value = value.slice(0, MAX_ARRAY_LENGTH_OF_SAMPLE_DOCUMENT_VALUE); + } + // Recursively truncate strings in nested objects or arrays. + for (const key in value) { + if (value.hasOwnProperty(key)) { + value[key] = truncate(value[key]); + } + } + } + return value; + } + + return truncate(obj); +} + +export async function getStringifiedSampleDocuments({ + prompt, + sampleDocuments, +}: { + prompt: string; + sampleDocuments: Document[]; +}): Promise { + if (!sampleDocuments.length) { + return ''; + } + + const model = await getCopilotModel(); + if (!model) { + return ''; + } + + let additionToPrompt: Document[] | Document = sampleDocuments; + let promptInputTokens = await model.countTokens( + prompt + toJSString(sampleDocuments) + ); + + // First check the length of all stringified sample documents. + // If the resulting prompt is too large, proceed with only 1 sample document. + // We also convert an array that contains only 1 element to a single document. + if ( + promptInputTokens > model.maxInputTokens || + sampleDocuments.length === 1 + ) { + additionToPrompt = sampleDocuments[0]; + } + + const stringifiedDocuments = toJSString(additionToPrompt); + promptInputTokens = await model.countTokens(prompt + stringifiedDocuments); + + // Add sample documents to the prompt only when it fits in the context window. + if (promptInputTokens <= model.maxInputTokens) { + return `Sample document${ + Array.isArray(additionToPrompt) ? 's' : '' + }: ${stringifiedDocuments}\n`; + } + + return ''; +} diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index f26cdc11c..1ad4924bb 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -11,6 +11,7 @@ import * as vscode from 'vscode'; import { loadFixturesToDB, reloadFixture } from './fixtures/fixture-loader'; import type { Fixtures } from './fixtures/fixture-loader'; import { AIBackend } from './ai-backend'; +import type { ChatCompletion } from './ai-backend'; import { GenericPrompt } from '../../participant/prompts/generic'; import { QueryPrompt } from '../../participant/prompts/query'; import { @@ -106,7 +107,7 @@ const queryTestCases: TestCase[] = [ assertResult: async ({ responseContent, connectionString, - }: AssertProps) => { + }: AssertProps): Promise => { const result = await runCodeInMessage(responseContent, connectionString); const totalResponse = `${result.printOutput.join('')}${ @@ -242,7 +243,7 @@ async function pushResultsToDB({ anyFailedAccuracyThreshold: boolean; runTimeMS: number; httpErrors: number; -}) { +}): Promise { const client = new MongoClient( process.env.AI_ACCURACY_RESULTS_MONGODB_CONNECTION_STRING || '' ); @@ -278,13 +279,13 @@ async function pushResultsToDB({ } } -const buildMessages = ({ +const buildMessages = async ({ testCase, fixtures, }: { testCase: TestCase; fixtures: Fixtures; -}) => { +}): Promise => { switch (testCase.type) { case 'generic': return GenericPrompt.buildMessages({ @@ -293,7 +294,7 @@ const buildMessages = ({ }); case 'query': - return QueryPrompt.buildMessages({ + return await QueryPrompt.buildMessages({ request: { prompt: testCase.userInput }, context: { history: [] }, databaseName: testCase.databaseName, @@ -329,8 +330,8 @@ async function runTest({ testCase: TestCase; aiBackend: AIBackend; fixtures: Fixtures; -}) { - const messages = buildMessages({ +}): Promise { + const messages = await buildMessages({ testCase, fixtures, }); diff --git a/src/test/ai-accuracy-tests/ai-backend.ts b/src/test/ai-accuracy-tests/ai-backend.ts index a9b746e1f..02534f580 100644 --- a/src/test/ai-accuracy-tests/ai-backend.ts +++ b/src/test/ai-accuracy-tests/ai-backend.ts @@ -4,7 +4,7 @@ import type { ChatCompletionCreateParamsBase } from 'openai/resources/chat/compl import { CHAT_PARTICIPANT_MODEL } from '../../participant/constants'; let openai: OpenAI; -function getOpenAIClient() { +function getOpenAIClient(): OpenAI { if (!openai) { openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, @@ -22,7 +22,7 @@ type ChatMessage = { }; type ChatMessages = ChatMessage[]; -type ChatCompletion = { +export type ChatCompletion = { content: string; usageStats: { promptTokens: number; diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 137ba7851..761524a44 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -3,6 +3,7 @@ import { beforeEach, afterEach } from 'mocha'; import { expect } from 'chai'; import sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; +import { ObjectId, Int32 } from 'bson'; import ParticipantController, { parseForDatabaseAndCollectionName, @@ -22,6 +23,10 @@ import { } from '../../../storage/storageController'; import type { LoadedConnection } from '../../../storage/connectionStorage'; +// The Copilot's model in not available in tests, +// therefore we need to mock its methods and returning values. +export const MAX_TOTAL_PROMPT_LENGTH = 16000; + const loadedConnection = { id: 'id', name: 'localhost', @@ -44,6 +49,7 @@ suite('Participant Controller Test Suite', function () { let chatContextStub; let chatStreamStub; let chatTokenStub; + let countTokensStub; let sendRequestStub; beforeEach(function () { @@ -76,8 +82,9 @@ suite('Participant Controller Test Suite', function () { button: sinon.fake(), }; chatTokenStub = { - onCancellationRequested: () => {}, + onCancellationRequested: sinon.fake(), }; + countTokensStub = sinon.stub(); // The model returned by vscode.lm.selectChatModels is always undefined in tests. sendRequestStub = sinon.fake.resolves({ text: [ @@ -97,8 +104,8 @@ suite('Participant Controller Test Suite', function () { family: 'gpt-4o', version: 'gpt-4o-date', name: 'GPT 4o (date)', - maxInputTokens: 16211, - countTokens: () => {}, + maxInputTokens: MAX_TOTAL_PROMPT_LENGTH, + countTokens: countTokensStub, sendRequest: sendRequestStub, }, ]) @@ -293,7 +300,10 @@ suite('Participant Controller Test Suite', function () { }); suite('when connected', function () { + let sampleStub; + beforeEach(function () { + sampleStub = sinon.stub(); sinon.replace( testParticipantController._connectionController, 'getActiveDataService', @@ -331,13 +341,7 @@ suite('Participant Controller Test Suite', function () { url: TEST_DATABASE_URI, options: {}, }), - sample: () => - Promise.resolve([ - { - _id: '66b3408a60da951fc354743e', - field: { subField: '66b3408a60da951fc354743e' }, - }, - ]), + sample: sampleStub, once: sinon.stub(), } as unknown as DataService) ); @@ -449,6 +453,16 @@ suite('Participant Controller Test Suite', function () { sinon .stub(testParticipantController, '_queryGenerationState') .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203660'), + field: { + stringField: + 'There was a house cat who finally got the chance to do what it had always wanted to do.', + arrayField: [new Int32('1')], + }, + }, + ]); const chatRequestMock = { prompt: 'find all docs by a name example', command: 'query', @@ -462,11 +476,248 @@ suite('Participant Controller Test Suite', function () { ); const messages = sendRequestStub.firstCall.args[0]; expect(messages[0].content).to.include( - 'Collection schema:\n' + - '_id: String\n' + - 'field.subField: String\n' + 'Collection schema: _id: ObjectId\n' + + 'field.stringField: String\n' + + 'field.arrayField: Array\n' ); }); + + suite('useSampleDocsInCopilot setting is true', function () { + beforeEach(async () => { + await vscode.workspace + .getConfiguration('mdb') + .update('useSampleDocsInCopilot', true); + }); + + afterEach(async () => { + await vscode.workspace + .getConfiguration('mdb') + .update('useSampleDocsInCopilot', false); + }); + + test('includes 3 sample documents as an array', async function () { + sinon + .stub(testParticipantController, '_queryGenerationState') + .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); + countTokensStub.resolves(MAX_TOTAL_PROMPT_LENGTH); + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203661'), + field: { + stringField: 'Text 1', + }, + }, + { + _id: new ObjectId('63ed1d522d8573fa5c203662'), + field: { + stringField: 'Text 2', + }, + }, + { + _id: new ObjectId('63ed1d522d8573fa5c203663'), + field: { + stringField: 'Text 3', + }, + }, + ]); + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const messages = sendRequestStub.firstCall.args[0]; + expect(messages[0].content).to.include( + 'Sample documents: [\n' + + ' {\n' + + " _id: ObjectId('63ed1d522d8573fa5c203661'),\n" + + ' field: {\n' + + " stringField: 'Text 1'\n" + + ' }\n' + + ' },\n' + + ' {\n' + + " _id: ObjectId('63ed1d522d8573fa5c203662'),\n" + + ' field: {\n' + + " stringField: 'Text 2'\n" + + ' }\n' + + ' },\n' + + ' {\n' + + " _id: ObjectId('63ed1d522d8573fa5c203663'),\n" + + ' field: {\n' + + " stringField: 'Text 3'\n" + + ' }\n' + + ' }\n' + + ']\n' + ); + }); + + test('includes 1 sample document as an object', async function () { + sinon + .stub(testParticipantController, '_queryGenerationState') + .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); + countTokensStub.resolves(MAX_TOTAL_PROMPT_LENGTH); + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203660'), + field: { + stringField: + 'There was a house cat who finally got the chance to do what it had always wanted to do.', + arrayField: [ + new Int32('1'), + new Int32('2'), + new Int32('3'), + new Int32('4'), + new Int32('5'), + new Int32('6'), + new Int32('7'), + new Int32('8'), + new Int32('9'), + ], + }, + }, + ]); + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const messages = sendRequestStub.firstCall.args[0]; + expect(messages[0].content).to.include( + 'Sample document: {\n' + + " _id: ObjectId('63ed1d522d8573fa5c203660'),\n" + + ' field: {\n' + + " stringField: 'There was a house ca',\n" + + ' arrayField: [\n' + + " NumberInt('1'),\n" + + " NumberInt('2'),\n" + + " NumberInt('3')\n" + + ' ]\n' + + ' }\n' + + '}\n' + ); + }); + + test('includes 1 sample documents when 3 make prompt too long', async function () { + sinon + .stub(testParticipantController, '_queryGenerationState') + .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); + countTokensStub.onCall(0).resolves(MAX_TOTAL_PROMPT_LENGTH + 1); + countTokensStub.onCall(1).resolves(MAX_TOTAL_PROMPT_LENGTH); + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203661'), + field: { + stringField: 'Text 1', + }, + }, + { + _id: new ObjectId('63ed1d522d8573fa5c203662'), + field: { + stringField: 'Text 2', + }, + }, + { + _id: new ObjectId('63ed1d522d8573fa5c203663'), + field: { + stringField: 'Text 3', + }, + }, + ]); + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const messages = sendRequestStub.firstCall.args[0]; + expect(messages[0].content).to.include( + 'Sample document: {\n' + + " _id: ObjectId('63ed1d522d8573fa5c203661'),\n" + + ' field: {\n' + + " stringField: 'Text 1'\n" + + ' }\n' + + '}\n' + ); + }); + + test('does not include sample documents when even 1 makes prompt too long', async function () { + sinon + .stub(testParticipantController, '_queryGenerationState') + .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); + countTokensStub.onCall(0).resolves(MAX_TOTAL_PROMPT_LENGTH + 1); + countTokensStub.onCall(1).resolves(MAX_TOTAL_PROMPT_LENGTH + 1); + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203661'), + field: { + stringField: 'Text 1', + }, + }, + { + _id: new ObjectId('63ed1d522d8573fa5c203662'), + field: { + stringField: 'Text 2', + }, + }, + { + _id: new ObjectId('63ed1d522d8573fa5c203663'), + field: { + stringField: 'Text 3', + }, + }, + ]); + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const messages = sendRequestStub.firstCall.args[0]; + expect(messages[0].content).to.not.include('Sample documents'); + }); + }); + + suite('useSampleDocsInCopilot setting is false', function () { + test('does not include sample documents', async function () { + sinon + .stub(testParticipantController, '_queryGenerationState') + .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + }; + await testParticipantController.chatHandler( + chatRequestMock, + chatContextStub, + chatStreamStub, + chatTokenStub + ); + const messages = sendRequestStub.firstCall.args[0]; + expect(messages[0].content).to.not.include('Sample documents'); + }); + }); }); suite('unknown namespace', function () { From ab9d5b77c08381dbfdc5c25c0e74ee3b673a23eb Mon Sep 17 00:00:00 2001 From: Rhys Date: Fri, 6 Sep 2024 15:19:12 -0400 Subject: [PATCH 18/45] chore(participant): add sample docs test, add debug prompt var VSCODE-583 (#809) --- .../ai-accuracy-tests/ai-accuracy-tests.ts | 50 +++++++++++-- src/test/ai-accuracy-tests/assertions.ts | 11 ++- .../create-test-results-html-page.ts | 4 +- .../ai-accuracy-tests/fixtures/antiques.ts | 74 +++++++++++++++++++ .../fixtures/fixture-loader.ts | 4 +- src/test/ai-accuracy-tests/test-setup.ts | 16 +++- 6 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 src/test/ai-accuracy-tests/fixtures/antiques.ts diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index 1ad4924bb..981a21544 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -25,6 +25,9 @@ import { parseForDatabaseAndCollectionName } from '../../participant/participant const numberOfRunsPerTest = 1; +// When true, we will log the entire prompt we send to the model for each test. +const DEBUG_PROMPTS = process.env.DEBUG_PROMPTS === 'true'; + type AssertProps = { responseContent: string; connectionString: string; @@ -42,6 +45,7 @@ type TestCase = { reloadFixtureOnEachRun?: boolean; databaseName?: string; collectionName?: string; + includeSampleDocuments?: boolean; accuracyThresholdOverride?: number; assertResult: (props: AssertProps) => Promise | void; only?: boolean; // Translates to mocha's it.only so only this test will run. @@ -53,7 +57,7 @@ const namespaceTestCases: TestCase[] = [ type: 'namespace', userInput: 'How many documents are in the tempReadings collection in the pools database?', - assertResult: ({ responseContent }: AssertProps) => { + assertResult: ({ responseContent }: AssertProps): void => { const namespace = parseForDatabaseAndCollectionName(responseContent); expect(namespace.databaseName).to.equal('pools'); @@ -64,7 +68,7 @@ const namespaceTestCases: TestCase[] = [ testCase: 'No namespace included in basic query', type: 'namespace', userInput: 'How many documents are in the collection?', - assertResult: ({ responseContent }: AssertProps) => { + assertResult: ({ responseContent }: AssertProps): void => { const namespace = parseForDatabaseAndCollectionName(responseContent); expect(namespace.databaseName).to.equal(undefined); @@ -76,7 +80,7 @@ const namespaceTestCases: TestCase[] = [ type: 'namespace', userInput: 'How do I create a new user with read write permissions on the orders collection?', - assertResult: ({ responseContent }: AssertProps) => { + assertResult: ({ responseContent }: AssertProps): void => { const namespace = parseForDatabaseAndCollectionName(responseContent); expect(namespace.databaseName).to.equal(undefined); @@ -88,7 +92,7 @@ const namespaceTestCases: TestCase[] = [ type: 'namespace', userInput: 'How do I create a new user with read write permissions on the orders db?', - assertResult: ({ responseContent }: AssertProps) => { + assertResult: ({ responseContent }: AssertProps): void => { const namespace = parseForDatabaseAndCollectionName(responseContent); expect(namespace.databaseName).to.equal('orders'); @@ -131,7 +135,7 @@ const queryTestCases: TestCase[] = [ connectionString, mongoClient, fixtures, - }: AssertProps) => { + }: AssertProps): Promise => { const documentsBefore = await mongoClient .db('CookBook') .collection('recipes') @@ -172,7 +176,7 @@ const queryTestCases: TestCase[] = [ assertResult: async ({ responseContent, connectionString, - }: AssertProps) => { + }: AssertProps): Promise => { const output = await runCodeInMessage(responseContent, connectionString); expect(output.data?.result?.content[0]).to.deep.equal({ @@ -192,7 +196,7 @@ const queryTestCases: TestCase[] = [ responseContent, connectionString, mongoClient, - }: AssertProps) => { + }: AssertProps): Promise => { const indexesBefore = await mongoClient .db('FarmData') .collection('Pineapples') @@ -213,6 +217,27 @@ const queryTestCases: TestCase[] = [ ).to.have.keys(['harvestedDate', 'sweetnessScale']); }, }, + { + testCase: 'Aggregation with an or or $in, with sample docs', + type: 'query', + only: true, + databaseName: 'Antiques', + collectionName: 'items', + includeSampleDocuments: true, + reloadFixtureOnEachRun: true, + userInput: + 'which collectors specialize only in mint items? and are located in London or New York? an array of their names in a field called collectors', + assertResult: async ({ + responseContent, + connectionString, + }: AssertProps): Promise => { + const output = await runCodeInMessage(responseContent, connectionString); + + expect(output.data?.result?.content?.[0].collectors).to.have.lengthOf(2); + expect(output.data?.result?.content[0].collectors).to.include('John Doe'); + expect(output.data?.result?.content[0].collectors).to.include('Monkey'); + }, + }, ]; const testCases: TestCase[] = [...namespaceTestCases, ...queryTestCases]; @@ -309,6 +334,13 @@ const buildMessages = async ({ ]?.schema, } : {}), + ...(testCase.includeSampleDocuments + ? { + sampleDocuments: fixtures[testCase.databaseName as string][ + testCase.collectionName as string + ].documents.slice(0, 3), + } + : {}), }); case 'namespace': @@ -335,6 +367,10 @@ async function runTest({ testCase, fixtures, }); + if (DEBUG_PROMPTS) { + console.log('Messages to send to chat completion:'); + console.log(messages); + } const chatCompletion = await aiBackend.runAIChatCompletionGeneration({ messages: messages.map((message) => ({ ...message, diff --git a/src/test/ai-accuracy-tests/assertions.ts b/src/test/ai-accuracy-tests/assertions.ts index aa1289fc9..31460ef20 100644 --- a/src/test/ai-accuracy-tests/assertions.ts +++ b/src/test/ai-accuracy-tests/assertions.ts @@ -54,8 +54,10 @@ export const runCodeInMessage = async ( }; }; -export const isDeepStrictEqualTo = (expected: unknown) => (actual: unknown) => - assert.deepStrictEqual(actual, expected); +export const isDeepStrictEqualTo = + (expected: unknown) => + (actual: unknown): void => + assert.deepStrictEqual(actual, expected); export const isDeepStrictEqualToFixtures = ( @@ -64,13 +66,14 @@ export const isDeepStrictEqualToFixtures = fixtures: Fixtures, comparator: (document: Document) => boolean ) => - (actual: unknown) => { + (actual: unknown): void => { const expected = fixtures[db][coll].documents.filter(comparator); assert.deepStrictEqual(actual, expected); }; export const anyOf = - (assertions: ((result: unknown) => void)[]) => (actual: unknown) => { + (assertions: ((result: unknown) => void)[]) => + (actual: unknown): void => { const errors: Error[] = []; for (const assertion of assertions) { try { diff --git a/src/test/ai-accuracy-tests/create-test-results-html-page.ts b/src/test/ai-accuracy-tests/create-test-results-html-page.ts index 59b362e8f..f58a50950 100644 --- a/src/test/ai-accuracy-tests/create-test-results-html-page.ts +++ b/src/test/ai-accuracy-tests/create-test-results-html-page.ts @@ -23,7 +23,7 @@ export type TestOutputs = { [testName: string]: TestOutput; }; -function getTestResultsTable(testResults: TestResult[]) { +function getTestResultsTable(testResults: TestResult[]): string { const headers = Object.keys(testResults[0]) .map((key) => ``) .join(''); @@ -49,7 +49,7 @@ function getTestResultsTable(testResults: TestResult[]) { `; } -function getTestOutputTables(testOutputs: TestOutputs) { +function getTestOutputTables(testOutputs: TestOutputs): string { const outputTables = Object.entries(testOutputs) .map(([testName, output]) => { const outputRows = output.outputs diff --git a/src/test/ai-accuracy-tests/fixtures/antiques.ts b/src/test/ai-accuracy-tests/fixtures/antiques.ts new file mode 100644 index 000000000..81c5ef1d8 --- /dev/null +++ b/src/test/ai-accuracy-tests/fixtures/antiques.ts @@ -0,0 +1,74 @@ +import type { Fixture } from './fixture-type'; + +const antiques: Fixture = { + db: 'Antiques', + coll: 'items', + documents: [ + { + itemName: 'Vintage Beatles Vinyl', + owner: { + name: 'John Doe', + location: 'London', + }, + acquisition: { + date: '1998-03-13', + price: 1200, + }, + condition: 'Mint', + history: [ + { event: 'Auction Win', date: '1998-03-13' }, + { event: 'Restoration', date: '2005-07-22' }, + ], + }, + { + itemName: 'Ancient Roman Coin', + owner: { + name: 'Jane Doe', + location: 'Rome', + }, + acquisition: { + date: '2002-11-27', + price: 5000, + }, + condition: 'Good', + history: [ + { event: 'Found in a dig', date: '2002-11-27' }, + { event: 'Museum Display', date: '2010-02-15' }, + ], + }, + { + itemName: 'Victorian Pocket Watch', + owner: { + name: 'Arnold Arnoldson', + location: 'London', + }, + acquisition: { + date: '2010-06-30', + price: 800, + }, + condition: 'Fair', + history: [ + { event: 'Inherited', date: '2010-06-30' }, + { event: 'Repair', date: '2015-09-12' }, + ], + }, + { + itemName: 'An Ancient Pineapple (super rare)', + owner: { + name: 'Monkey', + location: 'New York', + }, + acquisition: { + date: '2018-02-05', + price: 2300, + }, + condition: 'Mint', + history: [ + { event: 'Estate Sale', date: '2018-02-05' }, + { event: 'Appraisal', date: '2020-04-18' }, + ], + }, + ], +}; + +export default antiques; diff --git a/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts b/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts index 527e9ae14..a97978a34 100644 --- a/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts +++ b/src/test/ai-accuracy-tests/fixtures/fixture-loader.ts @@ -2,6 +2,7 @@ import type { Document, MongoClient } from 'mongodb'; import { getSimplifiedSchema } from 'mongodb-schema'; import type { Fixture } from './fixture-type'; +import antiques from './antiques'; import pets from './pets'; import pineapples from './pineapples'; import recipes from './recipes'; @@ -19,6 +20,7 @@ export type Fixtures = { type LoadableFixture = (() => Fixture) | Fixture; const fixturesToLoad: LoadableFixture[] = [ + antiques, pets, pineapples, recipes, @@ -62,7 +64,7 @@ export async function reloadFixture({ coll: string; mongoClient: MongoClient; fixtures: Fixtures; -}) { +}): Promise { await mongoClient.db(db).collection(coll).drop(); const { documents } = fixtures[db][coll]; await mongoClient.db(db).collection(coll).insertMany(documents); diff --git a/src/test/ai-accuracy-tests/test-setup.ts b/src/test/ai-accuracy-tests/test-setup.ts index 07ce4a162..bd0a4d217 100644 --- a/src/test/ai-accuracy-tests/test-setup.ts +++ b/src/test/ai-accuracy-tests/test-setup.ts @@ -8,19 +8,29 @@ const vscodeMock = { User: UserRole, }, LanguageModelChatMessage: { - Assistant: (content, name?: string) => ({ + Assistant: (content, name?: string): unknown => ({ name, content, role: AssistantRole, }), - User: (content: string, name?: string) => ({ + User: (content: string, name?: string): unknown => ({ content, name, role: UserRole, }), }, window: { - createOutputChannel: () => {}, + createOutputChannel: (): void => {}, + }, + lm: { + selectChatModels: (): unknown => [ + { + countTokens: (input: string): number => { + return input.length; + }, + maxInputTokens: 10_000, + }, + ], }, }; From 3f720ab2f5fa87b368887b6d09df0da5b73f6348 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 10 Sep 2024 16:55:28 +0200 Subject: [PATCH 19/45] chore: merge main into VSCODE-528-mongodb-copilot (#814) --- .../actions/test-and-build/action.yaml | 4 +- .github/workflows/codeql.yml | 8 +- .github/workflows/draft-release.yaml | 4 +- .github/workflows/publish-release.yaml | 2 +- .github/workflows/rebuild-changelog.yaml | 2 +- .github/workflows/test-and-build.yaml | 4 +- package-lock.json | 193 ++++++++++-------- 7 files changed, 117 insertions(+), 100 deletions(-) diff --git a/.github/workflows/actions/test-and-build/action.yaml b/.github/workflows/actions/test-and-build/action.yaml index 85703a1ea..ce6d4bc08 100644 --- a/.github/workflows/actions/test-and-build/action.yaml +++ b/.github/workflows/actions/test-and-build/action.yaml @@ -30,7 +30,7 @@ runs: # Default Python (3.12) doesn't have support for distutils because of # which the dep install fails constantly on macos # https://github.com/nodejs/node-gyp/issues/2869 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -107,7 +107,7 @@ runs: shell: bash - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: VSIX built on ${{ runner.os }} path: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e7dd5af06..a7954643e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,11 +41,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -79,6 +79,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index 1d4f8b5c8..2246bc3ac 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -23,14 +23,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # NOTE: this is necessary to get the full history # and check if tags are already present fetch-depth: 0 - name: Setup Node.js Environment - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.16.0 diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index 787c02f3c..c9e8f1462 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Node.js Environment - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.16.0 diff --git a/.github/workflows/rebuild-changelog.yaml b/.github/workflows/rebuild-changelog.yaml index ad4637e68..6e6e11834 100644 --- a/.github/workflows/rebuild-changelog.yaml +++ b/.github/workflows/rebuild-changelog.yaml @@ -17,7 +17,7 @@ jobs: name: Rebuild changelog runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # don't checkout a detached HEAD, is important to have a real base # branch when creating a PR diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index a0e1b8c9f..ceaaa04f3 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -23,12 +23,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js Environment - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.16.0 diff --git a/package-lock.json b/package-lock.json index f40460af4..247c247e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2216,6 +2216,7 @@ "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", @@ -5709,25 +5710,28 @@ "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } @@ -5737,6 +5741,7 @@ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", "dev": true, + "license": "MIT", "dependencies": { "jest-diff": "^26.0.0", "pretty-format": "^26.0.0" @@ -5960,19 +5965,21 @@ } }, "node_modules/@types/yargs": { - "version": "15.0.15", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.15.tgz", - "integrity": "sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg==", + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/yauzl": { "version": "2.10.3", @@ -9298,6 +9305,7 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.14.2" } @@ -10859,6 +10867,71 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -13281,6 +13354,7 @@ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", @@ -13296,6 +13370,7 @@ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.14.2" } @@ -15426,6 +15501,18 @@ "which": "bin/which" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -16415,6 +16502,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", @@ -17225,77 +17313,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/run-applescript/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/run-applescript/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/run-applescript/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/run-applescript/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "engines": { - "node": ">=6" - } - }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", From c184beada576f50576458d5fcb073974cc57e712 Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 16 Sep 2024 17:12:35 -0400 Subject: [PATCH 20/45] chore: update listDatabases to use nameOnly: true (#818) --- src/explorer/connectionTreeItem.ts | 4 +++- src/participant/participant.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/explorer/connectionTreeItem.ts b/src/explorer/connectionTreeItem.ts index 680d89c2f..9c47423a4 100644 --- a/src/explorer/connectionTreeItem.ts +++ b/src/explorer/connectionTreeItem.ts @@ -115,7 +115,9 @@ export default class ConnectionTreeItem } try { - const dbs = await dataService.listDatabases(); + const dbs = await dataService.listDatabases({ + nameOnly: true, + }); return dbs.map((dbItem) => dbItem.name); } catch (error) { throw new Error( diff --git a/src/participant/participant.ts b/src/participant/participant.ts index de958f678..09534753f 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -323,7 +323,9 @@ export default class ParticipantController { } try { - const databases = await dataService.listDatabases(); + const databases = await dataService.listDatabases({ + nameOnly: true, + }); return databases.map((db) => ({ label: db.name, data: db.name, @@ -408,7 +410,9 @@ export default class ParticipantController { } try { - const databases = await dataService.listDatabases(); + const databases = await dataService.listDatabases({ + nameOnly: true, + }); return [ ...databases.slice(0, MAX_MARKDOWN_LIST_LENGTH).map((db) => this._createMarkdownLink({ From 764b8d153784b7e0dfb92d79f83517c0fa307625 Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 16 Sep 2024 18:25:50 -0400 Subject: [PATCH 21/45] chore(participant): refactor chat participant state VSCODE-583 (#810) --- src/connectionController.ts | 4 +- src/mdbExtensionController.ts | 36 +- src/participant/chatMetadata.ts | 36 + src/participant/constants.ts | 61 ++ src/participant/markdown.ts | 32 + src/participant/participant.ts | 663 +++++++++--------- src/participant/prompts/generic.ts | 8 +- src/participant/prompts/history.ts | 63 +- src/participant/prompts/namespace.ts | 43 +- src/participant/prompts/query.ts | 10 +- .../ai-accuracy-tests/ai-accuracy-tests.ts | 4 +- .../suite/participant/participant.test.ts | 513 ++++++++++---- 12 files changed, 952 insertions(+), 521 deletions(-) create mode 100644 src/participant/chatMetadata.ts create mode 100644 src/participant/markdown.ts diff --git a/src/connectionController.ts b/src/connectionController.ts index a4aa8783a..4905ab8a6 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -1054,7 +1054,7 @@ export default class ConnectionController { ); if (!selectedQuickPickItem) { - return true; + return false; } if (selectedQuickPickItem.data.type === NewConnectionType.NEW_CONNECTION) { @@ -1062,7 +1062,7 @@ export default class ConnectionController { } if (!selectedQuickPickItem.data.connectionId) { - return true; + return false; } const { successfullyConnected } = await this.connectWithConnectionId( diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 9574ae6f8..17cda055f 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -42,6 +42,7 @@ import WebviewController from './views/webviewController'; import { createIdFactory, generateId } from './utils/objectIdHelper'; import { ConnectionStorage } from './storage/connectionStorage'; import type StreamProcessorTreeItem from './explorer/streamProcessorTreeItem'; +import type { RunParticipantQueryCommandArgs } from './participant/participant'; import ParticipantController from './participant/participant'; // This class is the top-level controller for our extension. @@ -291,36 +292,47 @@ export default class MDBExtensionController implements vscode.Disposable { // ------ CHAT PARTICIPANT ------ // this.registerParticipantCommand( EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, - () => { + ({ runnableContent }: RunParticipantQueryCommandArgs) => { return this._playgroundController.createPlaygroundFromParticipantQuery({ - text: - this._participantController._chatResult?.metadata - ?.responseContent || '', + text: runnableContent, }); } ); this.registerParticipantCommand( EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, - () => { + ({ runnableContent }: RunParticipantQueryCommandArgs) => { return this._playgroundController.evaluateParticipantQuery( - this._participantController._chatResult?.metadata?.responseContent || - '' + runnableContent ); } ); this.registerCommand( EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, - (id: string) => this._participantController.connectWithParticipant(id) + (id?: string) => + this._participantController.connectWithParticipant( + id ? decodeURIComponent(id) : id + ) ); this.registerCommand( EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, - (name: string) => - this._participantController.selectDatabaseWithParticipant(name) + (_data: string) => { + const data = JSON.parse(decodeURIComponent(_data)); + return this._participantController.selectDatabaseWithParticipant({ + chatId: data.chatId, + databaseName: data.databaseName, + }); + } ); this.registerCommand( EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, - (name: string) => - this._participantController.selectCollectionWithParticipant(name) + (_data: string) => { + const data = JSON.parse(decodeURIComponent(_data)); + return this._participantController.selectCollectionWithParticipant({ + chatId: data.chatId, + databaseName: data.databaseName, + collectionName: data.collectionName, + }); + } ); }; diff --git a/src/participant/chatMetadata.ts b/src/participant/chatMetadata.ts new file mode 100644 index 000000000..29a196c36 --- /dev/null +++ b/src/participant/chatMetadata.ts @@ -0,0 +1,36 @@ +import * as vscode from 'vscode'; +import { v4 as uuidv4 } from 'uuid'; + +export type ChatMetadata = { + databaseName?: string; + collectionName?: string; +}; + +export class ChatMetadataStore { + _chats: { [chatId: string]: ChatMetadata } = {}; + + constructor() {} + + setChatMetadata(chatId: string, metadata: ChatMetadata): void { + this._chats[chatId] = metadata; + } + + getChatMetadata(chatId: string): ChatMetadata | undefined { + return this._chats[chatId]; + } + + static getChatIdFromHistoryOrNewChatId( + history: ReadonlyArray + ): string { + for (const historyItem of history) { + if ( + historyItem instanceof vscode.ChatResponseTurn && + historyItem.result?.metadata?.chatId + ) { + return historyItem.result.metadata.chatId; + } + } + + return uuidv4(); + } +} diff --git a/src/participant/constants.ts b/src/participant/constants.ts index d783a19d7..e9685e847 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -1,2 +1,63 @@ +import type * as vscode from 'vscode'; +import { ChatMetadataStore } from './chatMetadata'; + export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; + +export class NamespaceRequestChatResult implements vscode.ChatResult { + readonly metadata: { + chatId: string; + intent: 'askForNamespace'; + databaseName?: string | undefined; + collectionName?: string | undefined; + }; + + constructor({ + databaseName, + collectionName, + history, + }: { + history: ReadonlyArray; + databaseName: string | undefined; + collectionName: string | undefined; + }) { + this.metadata = { + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(history), + intent: 'askForNamespace', + databaseName, + collectionName, + }; + } +} + +export class EmptyRequestChatResult implements vscode.ChatResult { + readonly metadata: { + chatId: string; + intent: 'emptyRequest'; + }; + + constructor( + history: ReadonlyArray + ) { + this.metadata = { + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(history), + intent: 'emptyRequest', + }; + } +} + +export class AskToConnectChatResult implements vscode.ChatResult { + readonly metadata: { + chatId: string; + intent: 'askToConnect'; + }; + + constructor( + history: ReadonlyArray + ) { + this.metadata = { + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(history), + intent: 'askToConnect', + }; + } +} diff --git a/src/participant/markdown.ts b/src/participant/markdown.ts new file mode 100644 index 000000000..75ca9ed4b --- /dev/null +++ b/src/participant/markdown.ts @@ -0,0 +1,32 @@ +import * as vscode from 'vscode'; + +export function createMarkdownLink({ + commandId, + data, + name, +}: { + commandId: string; + data?: + | { + [field: string]: any; + } + | string; + name: string; +}): vscode.MarkdownString { + const encodedData = data + ? encodeURIComponent( + `["${ + typeof data === 'string' + ? data + : encodeURIComponent(JSON.stringify(data)) + }"]` + ) + : undefined; + const commandQueryString = data ? `?${encodedData}` : ''; + const connName = new vscode.MarkdownString( + `- ${name}\n` + ); + connName.supportHtml = true; + connName.isTrusted = { enabledCommands: [commandId] }; + return connName; +} diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 09534753f..97ec53f44 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -9,39 +9,35 @@ import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; import { GenericPrompt } from './prompts/generic'; -import { CHAT_PARTICIPANT_ID } from './constants'; +import { + AskToConnectChatResult, + CHAT_PARTICIPANT_ID, + EmptyRequestChatResult, + NamespaceRequestChatResult, +} from './constants'; import { QueryPrompt } from './prompts/query'; import { COL_NAME_ID, DB_NAME_ID, NamespacePrompt } from './prompts/namespace'; import { SchemaFormatter } from './schema'; import { getSimplifiedSampleDocuments } from './sampleDocuments'; import { getCopilotModel } from './model'; +import { createMarkdownLink } from './markdown'; +import { ChatMetadataStore } from './chatMetadata'; const log = createLogger('participant'); -export enum QUERY_GENERATION_STATE { - DEFAULT = 'DEFAULT', - ASK_TO_CONNECT = 'ASK_TO_CONNECT', - ASK_FOR_DATABASE_NAME = 'ASK_FOR_DATABASE_NAME', - ASK_FOR_COLLECTION_NAME = 'ASK_FOR_COLLECTION_NAME', - CHANGE_DATABASE_NAME = 'CHANGE_DATABASE_NAME', - FETCH_SCHEMA = 'FETCH_SCHEMA', -} - const NUM_DOCUMENTS_TO_SAMPLE = 3; -interface ChatResult extends vscode.ChatResult { - metadata: { - responseContent?: string; - }; -} - interface NamespaceQuickPicks { label: string; data: string; } -const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)\n?`; -const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)\n?`; +export type RunParticipantQueryCommandArgs = { + runnableContent: string; +}; + +const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)`; +const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)`; const MAX_MARKDOWN_LIST_LENGTH = 10; @@ -68,12 +64,7 @@ export default class ParticipantController { _participant?: vscode.ChatParticipant; _connectionController: ConnectionController; _storageController: StorageController; - _queryGenerationState?: QUERY_GENERATION_STATE; - _chatResult?: ChatResult; - _databaseName?: string; - _collectionName?: string; - _schema?: string; - _sampleDocuments?: Document[]; + _chatMetadataStore: ChatMetadataStore; constructor({ connectionController, @@ -84,27 +75,7 @@ export default class ParticipantController { }) { this._connectionController = connectionController; this._storageController = storageController; - } - - _setDatabaseName(name: string | undefined): void { - if ( - this._queryGenerationState === QUERY_GENERATION_STATE.DEFAULT && - this._databaseName !== name - ) { - this._queryGenerationState = QUERY_GENERATION_STATE.CHANGE_DATABASE_NAME; - this._collectionName = undefined; - } - this._databaseName = name; - } - - _setCollectionName(name: string | undefined): void { - if ( - this._queryGenerationState === QUERY_GENERATION_STATE.DEFAULT && - this._collectionName !== name - ) { - this._queryGenerationState = QUERY_GENERATION_STATE.FETCH_SCHEMA; - } - this._collectionName = name; + this._chatMetadataStore = new ChatMetadataStore(); } createParticipant(context: vscode.ExtensionContext): vscode.ChatParticipant { @@ -130,43 +101,6 @@ export default class ParticipantController { return this._participant; } - async handleEmptyQueryRequest(): Promise<(string | vscode.MarkdownString)[]> { - const messages: (string | vscode.MarkdownString)[] = []; - switch (this._queryGenerationState) { - case QUERY_GENERATION_STATE.ASK_TO_CONNECT: - messages.push( - vscode.l10n.t( - 'Please select a cluster to connect by clicking on an item in the connections list.' - ) - ); - messages.push(...this.getConnectionsTree()); - break; - case QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME: - messages.push( - vscode.l10n.t( - 'Please select a database by either clicking on an item in the list or typing the name manually in the chat.' - ) - ); - messages.push(...(await this.getDatabasesTree())); - break; - case QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME: - messages.push( - vscode.l10n.t( - 'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.' - ) - ); - messages.push(...(await this.getCollectionTree())); - break; - default: - messages.push( - vscode.l10n.t( - 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".' - ) - ); - } - return messages; - } - handleError(err: any, stream: vscode.ChatResponseStream): void { // Making the chat request might fail because // - model does not exist @@ -190,6 +124,19 @@ export default class ParticipantController { } } + /** + * In order to get access to the model, and to write more messages to the chat after + * an async event that occurs after we've already completed our response, we need + * to be handling a chat request. This could be when a user clicks a button or link + * in the chat. To work around this, we can write a message as the user, which will + * trigger the chat handler and give us access to the model. + */ + writeChatMessageAsUser(message: string): Thenable { + return vscode.commands.executeCommand('workbench.action.chat.open', { + query: `@MongoDB ${message}`, + }); + } + async getChatResponseContent({ messages, stream, @@ -221,7 +168,7 @@ export default class ParticipantController { context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken - ): Promise { + ): Promise { const messages = GenericPrompt.buildMessages({ request, context, @@ -239,56 +186,48 @@ export default class ParticipantController { stream.markdown(responseContent); const runnableContent = getRunnableContentFromString(responseContent); - if (runnableContent && runnableContent.trim().length) { + if (runnableContent) { + const commandArgs: RunParticipantQueryCommandArgs = { + runnableContent, + }; stream.button({ command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, title: vscode.l10n.t('▶️ Run'), + arguments: [commandArgs], }); - stream.button({ command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, title: vscode.l10n.t('Open in playground'), + arguments: [commandArgs], }); - - return { metadata: { responseContent: runnableContent } }; } - return { metadata: {} }; + return { + metadata: { + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ), + }, + }; } async connectWithParticipant(id?: string): Promise { if (!id) { - await this._connectionController.changeActiveConnection(); + const didChangeActiveConnection = + await this._connectionController.changeActiveConnection(); + if (!didChangeActiveConnection) { + // If they don't choose a connection then we can't proceed; + return false; + } } else { await this._connectionController.connectWithConnectionId(id); } const connectionName = this._connectionController.getActiveConnectionName(); - if (connectionName) { - this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; - } - return vscode.commands.executeCommand('workbench.action.chat.open', { - query: `@MongoDB /query ${connectionName}`, - }); - } - - _createMarkdownLink({ - commandId, - query, - name, - }: { - commandId: string; - query?: string; - name: string; - }): vscode.MarkdownString { - const commandQuery = query ? `?%5B%22${query}%22%5D` : ''; - const connName = new vscode.MarkdownString( - `- ${name}\n` - ); - connName.supportHtml = true; - connName.isTrusted = { enabledCommands: [commandId] }; - return connName; + return this.writeChatMessageAsUser( + `/query ${connectionName}` + ) as Promise; } getConnectionsTree(): vscode.MarkdownString[] { @@ -302,14 +241,14 @@ export default class ParticipantController { }) .slice(0, MAX_MARKDOWN_LIST_LENGTH) .map((conn: LoadedConnection) => - this._createMarkdownLink({ - commandId: 'mdb.connectWithParticipant', - query: conn.id, + createMarkdownLink({ + commandId: EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, + data: conn.id, name: conn.name, }) ), - this._createMarkdownLink({ - commandId: 'mdb.connectWithParticipant', + createMarkdownLink({ + commandId: EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, name: 'Show more', }), ]; @@ -318,7 +257,8 @@ export default class ParticipantController { async getDatabaseQuickPicks(): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { - this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; + // Run a blank command to get the user to connect first. + void this.writeChatMessageAsUser('/query'); return []; } @@ -335,7 +275,7 @@ export default class ParticipantController { } } - async _selectDatabaseWithCommandPalette(): Promise { + async _selectDatabaseWithQuickPick(): Promise { const databases = await this.getDatabaseQuickPicks(); const selectedQuickPickItem = await vscode.window.showQuickPick(databases, { placeHolder: 'Select a database...', @@ -343,32 +283,42 @@ export default class ParticipantController { return selectedQuickPickItem?.data; } - async selectDatabaseWithParticipant(name: string): Promise { - if (!name) { - const selectedName = await this._selectDatabaseWithCommandPalette(); - this._setDatabaseName(selectedName); - } else { - this._setDatabaseName(name); + async selectDatabaseWithParticipant({ + chatId, + databaseName: _databaseName, + }: { + chatId: string; + databaseName?: string; + }): Promise { + let databaseName: string | undefined = _databaseName; + if (!databaseName) { + databaseName = await this._selectDatabaseWithQuickPick(); + if (!databaseName) { + return false; + } } - return vscode.commands.executeCommand('workbench.action.chat.open', { - query: `@MongoDB /query ${this._databaseName || ''}`, + this._chatMetadataStore.setChatMetadata(chatId, { + databaseName: databaseName, }); - } - async getCollectionQuickPicks(): Promise { - if (!this._databaseName) { - return []; - } + return this.writeChatMessageAsUser( + `/query ${databaseName}` + ) as Promise; + } + async getCollectionQuickPicks( + databaseName: string + ): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { - this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; + // Run a blank command to get the user to connect first. + void this.writeChatMessageAsUser('/query'); return []; } try { - const collections = await dataService.listCollections(this._databaseName); + const collections = await dataService.listCollections(databaseName); return collections.map((db) => ({ label: db.name, data: db.name, @@ -378,8 +328,10 @@ export default class ParticipantController { } } - async _selectCollectionWithCommandPalette(): Promise { - const collections = await this.getCollectionQuickPicks(); + async _selectCollectionWithQuickPick( + databaseName: string + ): Promise { + const collections = await this.getCollectionQuickPicks(databaseName); const selectedQuickPickItem = await vscode.window.showQuickPick( collections, { @@ -389,23 +341,37 @@ export default class ParticipantController { return selectedQuickPickItem?.data; } - async selectCollectionWithParticipant(name: string): Promise { - if (!name) { - const selectedName = await this._selectCollectionWithCommandPalette(); - this._setCollectionName(selectedName); - } else { - this._setCollectionName(name); + async selectCollectionWithParticipant({ + chatId, + databaseName, + collectionName: _collectionName, + }: { + chatId: string; + databaseName: string; + collectionName?: string; + }): Promise { + let collectionName: string | undefined = _collectionName; + if (!collectionName) { + collectionName = await this._selectCollectionWithQuickPick(databaseName); + if (!collectionName) { + return false; + } } - return vscode.commands.executeCommand('workbench.action.chat.open', { - query: `@MongoDB /query ${this._collectionName || ''}`, + this._chatMetadataStore.setChatMetadata(chatId, { + databaseName: databaseName, + collectionName: collectionName, }); + return this.writeChatMessageAsUser( + `/query ${collectionName}` + ) as Promise; } - async getDatabasesTree(): Promise { + async getDatabasesTree( + context: vscode.ChatContext + ): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { - this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; return []; } @@ -415,16 +381,26 @@ export default class ParticipantController { }); return [ ...databases.slice(0, MAX_MARKDOWN_LIST_LENGTH).map((db) => - this._createMarkdownLink({ - commandId: 'mdb.selectDatabaseWithParticipant', - query: db.name, + createMarkdownLink({ + commandId: EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, + data: { + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ), + databaseName: db.name, + }, name: db.name, }) ), ...(databases.length > MAX_MARKDOWN_LIST_LENGTH ? [ - this._createMarkdownLink({ - commandId: 'mdb.selectDatabaseWithParticipant', + createMarkdownLink({ + data: { + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ), + }, + commandId: EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, name: 'Show more', }), ] @@ -436,31 +412,41 @@ export default class ParticipantController { } } - async getCollectionTree(): Promise { - if (!this._databaseName) { - return []; - } - + async getCollectionTree( + databaseName: string, + context: vscode.ChatContext + ): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { - this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; return []; } try { - const collections = await dataService.listCollections(this._databaseName); + const collections = await dataService.listCollections(databaseName); return [ ...collections.slice(0, MAX_MARKDOWN_LIST_LENGTH).map((coll) => - this._createMarkdownLink({ - commandId: 'mdb.selectCollectionWithParticipant', - query: coll.name, + createMarkdownLink({ + commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, + data: { + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ), + databaseName, + collectionName: coll.name, + }, name: coll.name, }) ), ...(collections.length > MAX_MARKDOWN_LIST_LENGTH ? [ - this._createMarkdownLink({ - commandId: 'mdb.selectCollectionWithParticipant', + createMarkdownLink({ + commandId: + EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, + data: { + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ), + }, name: 'Show more', }), ] @@ -472,79 +458,26 @@ export default class ParticipantController { } } - _ifNewChatResetQueryGenerationState(context: vscode.ChatContext): void { - const isNewChat = !context.history.find( - (historyItem) => historyItem.participant === CHAT_PARTICIPANT_ID - ); - if (isNewChat) { - this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; - this._chatResult = undefined; - this._setDatabaseName(undefined); - this._setCollectionName(undefined); - } - } - - _waitingForUserToProvideNamespace(prompt: string): boolean { - if ( - !this._queryGenerationState || - ![ - QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME, - QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME, - QUERY_GENERATION_STATE.CHANGE_DATABASE_NAME, - ].includes(this._queryGenerationState) - ) { - return false; - } - - if ( - [ - QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME, - QUERY_GENERATION_STATE.CHANGE_DATABASE_NAME, - ].includes(this._queryGenerationState) - ) { - this._setDatabaseName(prompt); - if (!this._collectionName) { - this._queryGenerationState = - QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME; - return true; - } - return false; - } - - if ( - this._queryGenerationState === - QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME - ) { - this._setCollectionName(prompt); - if (!this._databaseName) { - this._queryGenerationState = - QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME; - return true; - } - this._queryGenerationState = QUERY_GENERATION_STATE.FETCH_SCHEMA; - return false; - } - - return false; - } - - async _shouldAskForNamespace( - request: vscode.ChatRequest, - context: vscode.ChatContext, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken - ): Promise { - if (this._waitingForUserToProvideNamespace(request.prompt)) { - return true; - } - - if (this._databaseName && this._collectionName) { - return false; - } - + async _getNamespaceFromChat({ + request, + context, + stream, + token, + }: { + request: vscode.ChatRequest; + context: vscode.ChatContext; + stream: vscode.ChatResponseStream; + token: vscode.CancellationToken; + }): Promise<{ + databaseName: string | undefined; + collectionName: string | undefined; + }> { const messagesWithNamespace = NamespacePrompt.buildMessages({ context, request, + connectionNames: this._connectionController + .getSavedConnections() + .map((connection) => connection.name), }); const responseContentWithNamespace = await this.getChatResponseContent({ messages: messagesWithNamespace, @@ -555,93 +488,98 @@ export default class ParticipantController { responseContentWithNamespace ); - this._setDatabaseName(namespace.databaseName || this._databaseName); - this._setCollectionName(namespace.collectionName || this._collectionName); - - if (namespace.databaseName && namespace.collectionName) { - this._queryGenerationState = QUERY_GENERATION_STATE.FETCH_SCHEMA; - return false; - } - - return true; + // See if there's a namespace set in the + // chat metadata we can fallback to if the model didn't find it. + const chatId = ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ); + const { + databaseName: databaseNameFromMetadata, + collectionName: collectionNameFromMetadata, + } = this._chatMetadataStore.getChatMetadata(chatId) ?? {}; + + return { + databaseName: namespace.databaseName ?? databaseNameFromMetadata, + collectionName: namespace.collectionName ?? collectionNameFromMetadata, + }; } - async _askForNamespace( - request: vscode.ChatRequest, - stream: vscode.ChatResponseStream - ): Promise { - const dataService = this._connectionController.getActiveDataService(); - if (!dataService) { - this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; - return; - } - + async _askForNamespace({ + context, + databaseName, + collectionName, + stream, + }: { + context: vscode.ChatContext; + databaseName: string | undefined; + collectionName: string | undefined; + stream: vscode.ChatResponseStream; + }): Promise { // If no database or collection name is found in the user prompt, // we retrieve the available namespaces from the current connection. // Users can then select a value by clicking on an item in the list. - if (!this._databaseName) { - const tree = await this.getDatabasesTree(); + if (!databaseName) { + const tree = await this.getDatabasesTree(context); stream.markdown( 'What is the name of the database you would like this query to run against?\n\n' ); for (const item of tree) { stream.markdown(item); } - this._queryGenerationState = QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME; - } else if (!this._collectionName) { - const tree = await this.getCollectionTree(); + } else if (!collectionName) { + const tree = await this.getCollectionTree(databaseName, context); stream.markdown( - 'Which collection would you like to query within this database?\n\n' + `Which collection would you like to query within ${databaseName}?\n\n` ); for (const item of tree) { stream.markdown(item); } - this._queryGenerationState = - QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME; } - return; + return new NamespaceRequestChatResult({ + databaseName, + collectionName, + history: context.history, + }); } - _shouldAskToConnectIfNotConnected( + _askToConnect( + context: vscode.ChatContext, stream: vscode.ChatResponseStream - ): boolean { - const dataService = this._connectionController.getActiveDataService(); - if (dataService) { - return false; - } - + ): vscode.ChatResult { const tree = this.getConnectionsTree(); stream.markdown( "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against.\n\n" ); + for (const item of tree) { stream.markdown(item); } - this._queryGenerationState = QUERY_GENERATION_STATE.ASK_TO_CONNECT; - return true; - } - - _shouldFetchCollectionSchema(): boolean { - return this._queryGenerationState === QUERY_GENERATION_STATE.FETCH_SCHEMA; + return new AskToConnectChatResult(context.history); } - async _fetchCollectionSchemaAndSampleDocuments( - abortSignal?: AbortSignal - ): Promise { - if (this._queryGenerationState === QUERY_GENERATION_STATE.FETCH_SCHEMA) { - this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; - } - + // The sample documents returned from this are simplified (strings and arrays shortened). + async _fetchCollectionSchemaAndSampleDocuments({ + abortSignal, + databaseName, + collectionName, + }: { + abortSignal; + databaseName: string; + collectionName: string; + }): Promise<{ + schema?: string; + sampleDocuments?: Document[]; + }> { const dataService = this._connectionController.getActiveDataService(); - if (!dataService || !this._databaseName || !this._collectionName) { - return; + if (!dataService) { + return {}; } try { const sampleDocuments = (await dataService?.sample?.( - `${this._databaseName}.${this._collectionName}`, + `${databaseName}.${collectionName}`, { query: {}, size: NUM_DOCUMENTS_TO_SAMPLE, @@ -652,20 +590,73 @@ export default class ParticipantController { } )) || []; - const schema = await getSimplifiedSchema(sampleDocuments); - this._schema = new SchemaFormatter().format(schema); + const unformattedSchema = await getSimplifiedSchema(sampleDocuments); + const schema = new SchemaFormatter().format(unformattedSchema); const useSampleDocsInCopilot = !!vscode.workspace .getConfiguration('mdb') .get('useSampleDocsInCopilot'); - if (useSampleDocsInCopilot) { - this._sampleDocuments = getSimplifiedSampleDocuments(sampleDocuments); - } + return { + sampleDocuments: useSampleDocsInCopilot + ? getSimplifiedSampleDocuments(sampleDocuments) + : undefined, + schema, + }; } catch (err: any) { - this._schema = undefined; - this._sampleDocuments = undefined; + log.error('Unable to fetch schema and sample documents', err); + return {}; + } + } + + async handleEmptyQueryRequest({ + context, + stream, + }: { + context: vscode.ChatContext; + stream: vscode.ChatResponseStream; + }): Promise { + const lastMessageMetaData: vscode.ChatResponseTurn | undefined = context + .history[context.history.length - 1] as vscode.ChatResponseTurn; + if ( + (lastMessageMetaData?.result as NamespaceRequestChatResult)?.metadata + ?.intent !== 'askForNamespace' + ) { + stream.markdown(GenericPrompt.getEmptyRequestResponse()); + return new EmptyRequestChatResult(context.history); + } + + // When the last message was asking for a database or collection name, + // we re-ask the question. + let tree: vscode.MarkdownString[]; + const databaseName = ( + lastMessageMetaData.result as NamespaceRequestChatResult + ).metadata.databaseName; + if (databaseName) { + stream.markdown( + vscode.l10n.t( + 'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.' + ) + ); + tree = await this.getCollectionTree(databaseName, context); + } else { + stream.markdown( + vscode.l10n.t( + 'Please select a database by either clicking on an item in the list or typing the name manually in the chat.' + ) + ); + tree = await this.getDatabasesTree(context); + } + + for (const item of tree) { + stream.markdown(item); } + + return new NamespaceRequestChatResult({ + databaseName, + collectionName: undefined, + history: context.history, + }); } // @MongoDB /query find all documents where the "address" has the word Broadway in it. @@ -674,24 +665,35 @@ export default class ParticipantController { context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken - ): Promise { - // TODO: Reset this._queryGenerationState to QUERY_GENERATION_STATE.DEFAULT - // when a command other than /query is called, as it disrupts the flow. - this._ifNewChatResetQueryGenerationState(context); + ): Promise { + if (!this._connectionController.getActiveDataService()) { + return this._askToConnect(context, stream); + } - if (this._shouldAskToConnectIfNotConnected(stream)) { - return { metadata: {} }; + if (!request.prompt || request.prompt.trim().length === 0) { + return this.handleEmptyQueryRequest({ + context, + stream, + }); } - const shouldAskForNamespace = await this._shouldAskForNamespace( + // We "prompt chain" to handle the query requests. + // First we ask the model to parse for the database and collection name. + // If they exist, we can then use them in our final completion. + // When they don't exist we ask the user for them. + const { databaseName, collectionName } = await this._getNamespaceFromChat({ request, context, stream, - token - ); - if (shouldAskForNamespace) { - await this._askForNamespace(request, stream); - return { metadata: {} }; + token, + }); + if (!databaseName || !collectionName) { + return await this._askForNamespace({ + context, + databaseName, + collectionName, + stream, + }); } const abortController = new AbortController(); @@ -699,19 +701,23 @@ export default class ParticipantController { abortController.abort(); }); - if (this._shouldFetchCollectionSchema()) { - await this._fetchCollectionSchemaAndSampleDocuments( - abortController.signal - ); - } + const { schema, sampleDocuments } = + await this._fetchCollectionSchemaAndSampleDocuments({ + abortSignal: abortController.signal, + databaseName, + collectionName, + }); const messages = await QueryPrompt.buildMessages({ request, context, - databaseName: this._databaseName, - collectionName: this._collectionName, - schema: this._schema, - sampleDocuments: this._sampleDocuments, + databaseName, + collectionName, + schema, + connectionNames: this._connectionController + .getSavedConnections() + .map((connection) => connection.name), + ...(sampleDocuments ? { sampleDocuments } : {}), }); const responseContent = await this.getChatResponseContent({ messages, @@ -720,21 +726,25 @@ export default class ParticipantController { }); stream.markdown(responseContent); - this._queryGenerationState = QUERY_GENERATION_STATE.DEFAULT; const runnableContent = getRunnableContentFromString(responseContent); - if (runnableContent && runnableContent.trim().length) { + if (runnableContent) { + const commandArgs: RunParticipantQueryCommandArgs = { + runnableContent, + }; stream.button({ command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, title: vscode.l10n.t('▶️ Run'), + arguments: [commandArgs], }); stream.button({ command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, title: vscode.l10n.t('Open in playground'), + arguments: [commandArgs], }); } - return { metadata: { responseContent: runnableContent } }; + return { metadata: {} }; } async chatHandler( @@ -744,15 +754,15 @@ export default class ParticipantController { vscode.ChatResponseStream, vscode.CancellationToken ] - ): Promise { + ): Promise { const [request, , stream] = args; - if (!request.prompt || request.prompt.trim().length === 0) { - const messages = await this.handleEmptyQueryRequest(); - for (const msg of messages) { - stream.markdown(msg); - } - return { metadata: {} }; + if ( + !request.command && + (!request.prompt || request.prompt.trim().length === 0) + ) { + stream.markdown(GenericPrompt.getEmptyRequestResponse()); + return new EmptyRequestChatResult(args[1].history); } const hasBeenShownWelcomeMessageAlready = !!this._storageController.get( @@ -772,15 +782,12 @@ export default class ParticipantController { } if (request.command === 'query') { - this._chatResult = await this.handleQueryRequest(...args); + return await this.handleQueryRequest(...args); } else if (request.command === 'docs') { // TODO(VSCODE-570): Implement this. } else if (request.command === 'schema') { // TODO(VSCODE-571): Implement this. - } else { - this._chatResult = await this.handleGenericRequest(...args); } - - return this._chatResult; + return await this.handleGenericRequest(...args); } } diff --git a/src/participant/prompts/generic.ts b/src/participant/prompts/generic.ts index 3299e2426..d176fead2 100644 --- a/src/participant/prompts/generic.ts +++ b/src/participant/prompts/generic.ts @@ -21,12 +21,18 @@ Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; return vscode.LanguageModelChatMessage.User(prompt); } + static getEmptyRequestResponse(): string { + // TODO(VSCODE-572): Generic empty response handler + return vscode.l10n.t( + 'Ask anything about MongoDB, from writing queries to questions about your cluster.' + ); + } + static buildMessages({ context, request, }: { request: { - // vscode.ChatRequest prompt: string; }; context: vscode.ChatContext; diff --git a/src/participant/prompts/history.ts b/src/participant/prompts/history.ts index ea2c9d48d..fe13d2cd6 100644 --- a/src/participant/prompts/history.ts +++ b/src/participant/prompts/history.ts @@ -1,37 +1,70 @@ import * as vscode from 'vscode'; +import type { + AskToConnectChatResult, + EmptyRequestChatResult, + NamespaceRequestChatResult, +} from '../constants'; -import { CHAT_PARTICIPANT_ID } from '../constants'; - +// When passing the history to the model we only want contextual messages +// to be passed. This function parses through the history and returns +// the messages that are valuable to keep. +// eslint-disable-next-line complexity export function getHistoryMessages({ + connectionNames, context, }: { + connectionNames?: string[]; // Used to scrape the connecting messages from the history. context: vscode.ChatContext; }): vscode.LanguageModelChatMessage[] { const messages: vscode.LanguageModelChatMessage[] = []; - context.history.map((historyItem) => { - if ( - historyItem.participant === CHAT_PARTICIPANT_ID && - historyItem instanceof vscode.ChatRequestTurn - ) { + for (const historyItem of context.history) { + if (historyItem instanceof vscode.ChatRequestTurn) { + if ( + historyItem.prompt?.trim().length === 0 || + connectionNames?.includes(historyItem.prompt) + ) { + // When the message is empty or a connection name then we skip it. + // It's probably going to be the response to the connect step. + continue; + } + // eslint-disable-next-line new-cap messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt)); } if (historyItem instanceof vscode.ChatResponseTurn) { - let res = ''; + let message = ''; + + if ( + (historyItem.result as EmptyRequestChatResult).metadata?.intent === + 'emptyRequest' || + (historyItem.result as AskToConnectChatResult).metadata?.intent === + 'askToConnect' + ) { + // Skip a response to an empty user prompt message or connect message. + continue; + } + for (const fragment of historyItem.response) { - if ( - fragment instanceof vscode.ChatResponseMarkdownPart && - historyItem.result.metadata?.responseContent - ) { - res += fragment.value.value; + if (fragment instanceof vscode.ChatResponseMarkdownPart) { + message += fragment.value.value; + + if ( + (historyItem.result as NamespaceRequestChatResult).metadata + ?.intent === 'askForNamespace' + ) { + // When the message is the assistant asking for part of a namespace, + // we only want to include the question asked, not the user's + // database and collection names in the history item. + break; + } } } // eslint-disable-next-line new-cap - messages.push(vscode.LanguageModelChatMessage.Assistant(res)); + messages.push(vscode.LanguageModelChatMessage.Assistant(message)); } - }); + } return messages; } diff --git a/src/participant/prompts/namespace.ts b/src/participant/prompts/namespace.ts index 804c9e6f0..f311d5c2b 100644 --- a/src/participant/prompts/namespace.ts +++ b/src/participant/prompts/namespace.ts @@ -8,39 +8,33 @@ export const COL_NAME_ID = 'COLLECTION_NAME'; export class NamespacePrompt { static getAssistantPrompt(): vscode.LanguageModelChatMessage { const prompt = `You are a MongoDB expert. -Parse the user's prompt to find database and collection names. +Parse all user messages to find a database name and a collection name. Respond in the format: ${DB_NAME_ID}: X ${COL_NAME_ID}: Y where X and Y are the respective names. -Do not treat any user prompt as a database name. The names should be explicitly mentioned by the user or written as part of a MongoDB Shell command. If you cannot find the names do not imagine names. If only one of the names is found, respond only with the found name. Your response must be concise and correct. +When no names are found, respond with: +No names found. + ___ Example 1: - User: How many documents are in the sightings collection in the ufo database? - Response: ${DB_NAME_ID}: ufo ${COL_NAME_ID}: sightings - ___ Example 2: - User: How do I create an index in my pineapples collection? - Response: ${COL_NAME_ID}: pineapples - ___ Example 3: - User: Where is the best hummus in Berlin? - Response: No names found. `; @@ -57,16 +51,41 @@ No names found. static buildMessages({ context, request, + connectionNames, }: { request: { prompt: string; }; context: vscode.ChatContext; + connectionNames: string[]; }): vscode.LanguageModelChatMessage[] { + let historyMessages = getHistoryMessages({ context, connectionNames }); + // If the current user's prompt is a connection name, and the last + // message was to connect. We want to use the last + // message they sent before the connection name as their prompt. + let userPrompt = request.prompt; + if ( + connectionNames.includes(request.prompt) && + (context.history[context.history.length - 1] as vscode.ChatResponseTurn) + ?.result?.metadata?.askToConnect + ) { + // Go through the history in reverse order to find the last user message. + for (let i = historyMessages.length - 1; i >= 0; i--) { + if ( + historyMessages[i].role === vscode.LanguageModelChatMessageRole.User + ) { + userPrompt = historyMessages[i].content; + // Remove the item from the history messages array. + historyMessages = historyMessages.slice(0, i); + break; + } + } + } + const messages = [ NamespacePrompt.getAssistantPrompt(), - ...getHistoryMessages({ context }), - NamespacePrompt.getUserPrompt(request.prompt), + ...historyMessages, + NamespacePrompt.getUserPrompt(userPrompt), ]; return messages; diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index 18ce1c812..9a1e3ebeb 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -81,6 +81,7 @@ db.getCollection('');\n\n`; collectionName, schema, sampleDocuments, + connectionNames, }: { request: { prompt: string; @@ -90,6 +91,7 @@ db.getCollection('');\n\n`; collectionName?: string; schema?: string; sampleDocuments?: Document[]; + connectionNames: string[]; }): Promise { const messages = [ await QueryPrompt.getAssistantPrompt({ @@ -98,10 +100,16 @@ db.getCollection('');\n\n`; schema, sampleDocuments, }), - ...getHistoryMessages({ context }), + ...getHistoryMessages({ context, connectionNames }), QueryPrompt.getUserPrompt(request.prompt), ]; return messages; } + + static getEmptyRequestResponse(): string { + return vscode.l10n.t( + 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".' + ); + } } diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index 981a21544..712a98647 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -220,11 +220,9 @@ const queryTestCases: TestCase[] = [ { testCase: 'Aggregation with an or or $in, with sample docs', type: 'query', - only: true, databaseName: 'Antiques', collectionName: 'items', includeSampleDocuments: true, - reloadFixtureOnEachRun: true, userInput: 'which collectors specialize only in mint items? and are located in London or New York? an array of their names in a field called collectors', assertResult: async ({ @@ -324,6 +322,7 @@ const buildMessages = async ({ context: { history: [] }, databaseName: testCase.databaseName, collectionName: testCase.collectionName, + connectionNames: [], ...(fixtures[testCase.databaseName as string]?.[ testCase.collectionName as string ]?.schema @@ -347,6 +346,7 @@ const buildMessages = async ({ return NamespacePrompt.buildMessages({ request: { prompt: testCase.userInput }, context: { history: [] }, + connectionNames: [], }); default: diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 761524a44..378979119 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -8,7 +8,6 @@ import { ObjectId, Int32 } from 'bson'; import ParticipantController, { parseForDatabaseAndCollectionName, getRunnableContentFromString, - QUERY_GENERATION_STATE, } from '../../../participant/participant'; import ConnectionController from '../../../connectionController'; import { StorageController } from '../../../storage'; @@ -25,7 +24,7 @@ import type { LoadedConnection } from '../../../storage/connectionStorage'; // The Copilot's model in not available in tests, // therefore we need to mock its methods and returning values. -export const MAX_TOTAL_PROMPT_LENGTH = 16000; +const MAX_TOTAL_PROMPT_LENGTH_MOCK = 16000; const loadedConnection = { id: 'id', @@ -86,7 +85,7 @@ suite('Participant Controller Test Suite', function () { }; countTokensStub = sinon.stub(); // The model returned by vscode.lm.selectChatModels is always undefined in tests. - sendRequestStub = sinon.fake.resolves({ + sendRequestStub = sinon.stub().resolves({ text: [ '```javascript\n' + "use('dbOne');\n" + @@ -104,7 +103,7 @@ suite('Participant Controller Test Suite', function () { family: 'gpt-4o', version: 'gpt-4o-date', name: 'GPT 4o (date)', - maxInputTokens: MAX_TOTAL_PROMPT_LENGTH, + maxInputTokens: MAX_TOTAL_PROMPT_LENGTH_MOCK, countTokens: countTokensStub, sendRequest: sendRequestStub, }, @@ -175,7 +174,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( + const chatResult = await testParticipantController.chatHandler( chatRequestMock, chatContextStub, chatStreamStub, @@ -193,12 +192,14 @@ suite('Participant Controller Test Suite', function () { expect(showMoreMessage.value).to.include( '- Show more' ); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.be.eql(undefined); - expect(testParticipantController._queryGenerationState).to.be.eql( - QUERY_GENERATION_STATE.ASK_TO_CONNECT - ); + expect(chatResult?.metadata?.chatId.length).to.equal(36); + expect({ + ...chatResult?.metadata, + chatId: undefined, + }).to.deep.equal({ + intent: 'askToConnect', + chatId: undefined, + }); }); test('shows only 10 connections with the show more option', async function () { @@ -216,7 +217,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( + const chatResult = await testParticipantController.chatHandler( chatRequestMock, chatContextStub, chatStreamStub, @@ -235,12 +236,14 @@ suite('Participant Controller Test Suite', function () { '- Show more' ); expect(chatStreamStub.markdown.callCount).to.be.eql(12); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.be.eql(undefined); - expect(testParticipantController._queryGenerationState).to.be.eql( - QUERY_GENERATION_STATE.ASK_TO_CONNECT - ); + expect(chatResult?.metadata?.chatId.length).to.equal(36); + expect({ + ...chatResult?.metadata, + chatId: undefined, + }).to.deep.equal({ + intent: 'askToConnect', + chatId: undefined, + }); }); test('handles empty connection name', async function () { @@ -250,15 +253,12 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( + const chatResult = await testParticipantController.chatHandler( chatRequestMock, chatContextStub, chatStreamStub, chatTokenStub ); - expect(testParticipantController._queryGenerationState).to.be.eql( - QUERY_GENERATION_STATE.ASK_TO_CONNECT - ); chatRequestMock.prompt = ''; await testParticipantController.chatHandler( @@ -270,7 +270,7 @@ suite('Participant Controller Test Suite', function () { const emptyMessage = chatStreamStub.markdown.getCall(3).args[0]; expect(emptyMessage).to.include( - 'Please select a cluster to connect by clicking on an item in the connections list.' + "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against" ); const listConnectionsMessage = chatStreamStub.markdown.getCall(4).args[0]; expect(listConnectionsMessage.value).to.include( @@ -280,12 +280,14 @@ suite('Participant Controller Test Suite', function () { expect(showMoreMessage.value).to.include( '- Show more' ); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.be.eql(undefined); - expect(testParticipantController._queryGenerationState).to.be.eql( - QUERY_GENERATION_STATE.ASK_TO_CONNECT - ); + expect(chatResult?.metadata?.chatId.length).to.equal(36); + expect({ + ...chatResult?.metadata, + chatId: undefined, + }).to.deep.equal({ + intent: 'askToConnect', + chatId: undefined, + }); }); test('calls connect by id for an existing connection', async function () { @@ -345,9 +347,6 @@ suite('Participant Controller Test Suite', function () { once: sinon.stub(), } as unknown as DataService) ); - sinon - .stub(testParticipantController, '_shouldAskToConnectIfNotConnected') - .returns(false); }); suite('when has not been shown a welcome message yet', function () { @@ -392,36 +391,32 @@ suite('Participant Controller Test Suite', function () { command: undefined, references: [], }; - expect(testParticipantController._queryGenerationState).to.be.equal( - undefined - ); - expect(testParticipantController._chatResult).to.be.equal(undefined); await testParticipantController.chatHandler( chatRequestMock, chatContextStub, chatStreamStub, chatTokenStub ); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.include( - "db.getCollection('collOne').find({ name: 'example' });" - ); + + expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ + command: 'mdb.runParticipantQuery', + title: '▶️ Run', + arguments: [ + { + runnableContent: + "use('dbOne');\ndb.getCollection('collOne').find({ name: 'example' });", + }, + ], + }); }); }); suite('query command', function () { - suite('known namespace', function () { + suite('known namespace from running namespace LLM', function () { beforeEach(function () { - sinon - .stub(testParticipantController, '_databaseName') - .value('dbOne'); - sinon - .stub(testParticipantController, '_collectionName') - .value('collOne'); - sinon - .stub(testParticipantController, '_shouldAskForNamespace') - .resolves(false); + sendRequestStub.onCall(0).resolves({ + text: ['DATABASE_NAME: dbOne\n', 'COLLECTION_NAME: collOne\n`'], + }); }); test('generates a query', async function () { @@ -430,29 +425,25 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - expect(testParticipantController._queryGenerationState).to.be.equal( - undefined - ); - expect(testParticipantController._chatResult).to.be.equal( - undefined - ); await testParticipantController.chatHandler( chatRequestMock, chatContextStub, chatStreamStub, chatTokenStub ); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.include( - "db.getCollection('collOne').find({ name: 'example' });" - ); + expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ + command: 'mdb.runParticipantQuery', + title: '▶️ Run', + arguments: [ + { + runnableContent: + "use('dbOne');\ndb.getCollection('collOne').find({ name: 'example' });", + }, + ], + }); }); test('includes a collection schema', async function () { - sinon - .stub(testParticipantController, '_queryGenerationState') - .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); sampleStub.resolves([ { _id: new ObjectId('63ed1d522d8573fa5c203660'), @@ -474,7 +465,7 @@ suite('Participant Controller Test Suite', function () { chatStreamStub, chatTokenStub ); - const messages = sendRequestStub.firstCall.args[0]; + const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.include( 'Collection schema: _id: ObjectId\n' + 'field.stringField: String\n' + @@ -496,10 +487,7 @@ suite('Participant Controller Test Suite', function () { }); test('includes 3 sample documents as an array', async function () { - sinon - .stub(testParticipantController, '_queryGenerationState') - .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); - countTokensStub.resolves(MAX_TOTAL_PROMPT_LENGTH); + countTokensStub.resolves(MAX_TOTAL_PROMPT_LENGTH_MOCK); sampleStub.resolves([ { _id: new ObjectId('63ed1d522d8573fa5c203661'), @@ -531,7 +519,7 @@ suite('Participant Controller Test Suite', function () { chatStreamStub, chatTokenStub ); - const messages = sendRequestStub.firstCall.args[0]; + const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.include( 'Sample documents: [\n' + ' {\n' + @@ -557,10 +545,7 @@ suite('Participant Controller Test Suite', function () { }); test('includes 1 sample document as an object', async function () { - sinon - .stub(testParticipantController, '_queryGenerationState') - .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); - countTokensStub.resolves(MAX_TOTAL_PROMPT_LENGTH); + countTokensStub.resolves(MAX_TOTAL_PROMPT_LENGTH_MOCK); sampleStub.resolves([ { _id: new ObjectId('63ed1d522d8573fa5c203660'), @@ -592,7 +577,7 @@ suite('Participant Controller Test Suite', function () { chatStreamStub, chatTokenStub ); - const messages = sendRequestStub.firstCall.args[0]; + const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.include( 'Sample document: {\n' + " _id: ObjectId('63ed1d522d8573fa5c203660'),\n" + @@ -609,11 +594,10 @@ suite('Participant Controller Test Suite', function () { }); test('includes 1 sample documents when 3 make prompt too long', async function () { - sinon - .stub(testParticipantController, '_queryGenerationState') - .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); - countTokensStub.onCall(0).resolves(MAX_TOTAL_PROMPT_LENGTH + 1); - countTokensStub.onCall(1).resolves(MAX_TOTAL_PROMPT_LENGTH); + countTokensStub + .onCall(0) + .resolves(MAX_TOTAL_PROMPT_LENGTH_MOCK + 1); + countTokensStub.onCall(1).resolves(MAX_TOTAL_PROMPT_LENGTH_MOCK); sampleStub.resolves([ { _id: new ObjectId('63ed1d522d8573fa5c203661'), @@ -645,7 +629,7 @@ suite('Participant Controller Test Suite', function () { chatStreamStub, chatTokenStub ); - const messages = sendRequestStub.firstCall.args[0]; + const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.include( 'Sample document: {\n' + " _id: ObjectId('63ed1d522d8573fa5c203661'),\n" + @@ -657,11 +641,12 @@ suite('Participant Controller Test Suite', function () { }); test('does not include sample documents when even 1 makes prompt too long', async function () { - sinon - .stub(testParticipantController, '_queryGenerationState') - .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); - countTokensStub.onCall(0).resolves(MAX_TOTAL_PROMPT_LENGTH + 1); - countTokensStub.onCall(1).resolves(MAX_TOTAL_PROMPT_LENGTH + 1); + countTokensStub + .onCall(0) + .resolves(MAX_TOTAL_PROMPT_LENGTH_MOCK + 1); + countTokensStub + .onCall(1) + .resolves(MAX_TOTAL_PROMPT_LENGTH_MOCK + 1); sampleStub.resolves([ { _id: new ObjectId('63ed1d522d8573fa5c203661'), @@ -693,16 +678,13 @@ suite('Participant Controller Test Suite', function () { chatStreamStub, chatTokenStub ); - const messages = sendRequestStub.firstCall.args[0]; + const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.not.include('Sample documents'); }); }); suite('useSampleDocsInCopilot setting is false', function () { test('does not include sample documents', async function () { - sinon - .stub(testParticipantController, '_queryGenerationState') - .value(QUERY_GENERATION_STATE.FETCH_SCHEMA); const chatRequestMock = { prompt: 'find all docs by a name example', command: 'query', @@ -714,7 +696,7 @@ suite('Participant Controller Test Suite', function () { chatStreamStub, chatTokenStub ); - const messages = sendRequestStub.firstCall.args[0]; + const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.not.include('Sample documents'); }); }); @@ -727,10 +709,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - expect(testParticipantController._queryGenerationState).to.be.equal( - undefined - ); - await testParticipantController.chatHandler( + const chatResult = await testParticipantController.chatHandler( chatRequestMock, chatContextStub, chatStreamStub, @@ -742,86 +721,239 @@ suite('Participant Controller Test Suite', function () { ); const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; expect(listDBsMessage.value).to.include( - '- dbOne' + '- dbOne' ); const showMoreDBsMessage = chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreDBsMessage.value).to.include( - '- Show more' + '- Show more'); expect(chatStreamStub.markdown.callCount).to.be.eql(12); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.be.eql(undefined); - expect(testParticipantController._queryGenerationState).to.be.eql( - QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME - ); + const firstChatId = chatResult?.metadata?.chatId; + expect(chatResult?.metadata?.chatId.length).to.equal(36); + expect({ + ...chatResult?.metadata, + chatId: undefined, + }).to.deep.equal({ + intent: 'askForNamespace', + collectionName: undefined, + databaseName: undefined, + chatId: undefined, + }); chatRequestMock.prompt = 'dbOne'; - await testParticipantController.chatHandler( + sendRequestStub.onCall(1).resolves({ + text: ['DATABASE_NAME: dbOne\n'], + }); + const chatResult2 = await testParticipantController.chatHandler( chatRequestMock, - chatContextStub, + { + history: [ + { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + } as vscode.ChatRequestTurn, + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), + { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: + 'What is the name of the database you would like this query to run against?', + } as vscode.MarkdownString, + }, + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', + chatId: firstChatId, + }, + }, + } as vscode.ChatResponseTurn + ), + ], + }, chatStreamStub, chatTokenStub ); - expect(testParticipantController._databaseName).to.be.equal( - 'dbOne' - ); const askForCollMessage = chatStreamStub.markdown.getCall(12).args[0]; expect(askForCollMessage).to.include( - 'Which collection would you like to query within this database?' + 'Which collection would you like to query within dbOne?' ); const listCollsMessage = chatStreamStub.markdown.getCall(13).args[0]; expect(listCollsMessage.value).to.include( - '- collOne' + '- collOne' ); const showMoreCollsMessage = chatStreamStub.markdown.getCall(23).args[0]; expect(showMoreCollsMessage.value).to.include( - '- Show more' + '- Show more'); expect(chatStreamStub.markdown.callCount).to.be.eql(24); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.be.eql(undefined); - expect(testParticipantController._queryGenerationState).to.be.eql( - QUERY_GENERATION_STATE.ASK_FOR_COLLECTION_NAME - ); + expect(chatResult2?.metadata?.chatId).to.equal(firstChatId); + expect({ + ...chatResult?.metadata, + chatId: undefined, + }).to.deep.equal({ + intent: 'askForNamespace', + collectionName: undefined, + databaseName: undefined, + chatId: undefined, + }); chatRequestMock.prompt = 'collOne'; + sendRequestStub.onCall(2).resolves({ + text: ['DATABASE_NAME: dbOne\n', 'COLLECTION_NAME: collOne\n`'], + }); await testParticipantController.chatHandler( chatRequestMock, - chatContextStub, + { + history: [ + Object.assign( + Object.create(vscode.ChatRequestTurn.prototype), + { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + } + ), + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), + { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: + 'Which database would you like to query within this database?', + } as vscode.MarkdownString, + }, + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', + }, + }, + } + ), + Object.assign( + Object.create(vscode.ChatRequestTurn.prototype), + { + prompt: 'dbOne', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + } + ), + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), + { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: + 'Which collection would you like to query within dbOne?', + } as vscode.MarkdownString, + }, + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', + databaseName: 'dbOne', + collectionName: 'collOne', + chatId: firstChatId, + }, + }, + } + ), + ], + }, chatStreamStub, chatTokenStub ); - expect(testParticipantController._collectionName).to.be.equal( - 'collOne' - ); - expect( - testParticipantController._chatResult?.metadata.responseContent - ).to.include( - "db.getCollection('collOne').find({ name: 'example' });" - ); + expect(chatStreamStub?.button.callCount).to.equal(2); + expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ + command: 'mdb.runParticipantQuery', + title: '▶️ Run', + arguments: [ + { + runnableContent: + "use('dbOne');\ndb.getCollection('collOne').find({ name: 'example' });", + }, + ], + }); + expect(chatStreamStub?.button.getCall(1).args[0]).to.deep.equal({ + command: 'mdb.openParticipantQueryInPlayground', + title: 'Open in playground', + arguments: [ + { + runnableContent: + "use('dbOne');\ndb.getCollection('collOne').find({ name: 'example' });", + }, + ], + }); }); test('handles empty database name', async function () { - sinon - .stub(testParticipantController, '_queryGenerationState') - .value(QUERY_GENERATION_STATE.ASK_FOR_DATABASE_NAME); - const chatRequestMock = { prompt: '', command: 'query', references: [], }; - await testParticipantController.chatHandler( + const chatResult = await testParticipantController.chatHandler( chatRequestMock, - chatContextStub, + { + history: [ + { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + } as vscode.ChatRequestTurn, + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), + { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: + 'What is the name of the database you would like this query to run against?', + } as vscode.MarkdownString, + }, + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', + chatId: 'pineapple', + }, + }, + } as vscode.ChatResponseTurn + ), + ], + }, chatStreamStub, chatTokenStub ); @@ -832,29 +964,100 @@ suite('Participant Controller Test Suite', function () { ); const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; expect(listDBsMessage.value).to.include( - '- dbOne' + '- dbOne' ); + const showMoreDBsMessage = + chatStreamStub.markdown.getCall(11).args[0]; + expect(showMoreDBsMessage.value).to.include( + '- collOne' + '- collOne' ); + const showMoreCollsMessage = + chatStreamStub.markdown.getCall(1).args[0]; + expect(showMoreCollsMessage.value).to.include( + '- = 10.14.2" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -5709,44 +5691,6 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "26.0.24", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", - "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-diff": "^26.0.0", - "pretty-format": "^26.0.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -5964,23 +5908,6 @@ "@types/webidl-conversions": "*" } }, - "node_modules/@types/yargs": { - "version": "15.0.19", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", - "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -9300,16 +9227,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -13349,32 +13266,6 @@ "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==" }, - "node_modules/jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jose": { "version": "4.15.7", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.7.tgz", @@ -16497,22 +16388,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", diff --git a/package.json b/package.json index 7b742b4a9..83e1250ae 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "theme": "dark" }, "enabledApiProposals": [ - "chatVariableResolver" + "chatParticipantAdditions" ], "license": "SEE LICENSE IN LICENSE.txt", "main": "./dist/extension.js", @@ -1211,7 +1211,6 @@ "@types/chai": "^4.3.17", "@types/debug": "^4.1.12", "@types/glob": "^7.2.0", - "@types/jest": "^26.0.24", "@types/micromatch": "^4.0.9", "@types/mkdirp": "^2.0.0", "@types/mocha": "^8.2.3", diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 17cda055f..a72aee212 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -123,6 +123,7 @@ export default class MDBExtensionController implements vscode.Disposable { this._participantController = new ParticipantController({ connectionController: this._connectionController, storageController: this._storageController, + telemetryService: this._telemetryService, }); this._editorsController = new EditorsController({ context, diff --git a/src/participant/constants.ts b/src/participant/constants.ts index e9685e847..39f560998 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -4,60 +4,82 @@ import { ChatMetadataStore } from './chatMetadata'; export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; -export class NamespaceRequestChatResult implements vscode.ChatResult { - readonly metadata: { - chatId: string; - intent: 'askForNamespace'; - databaseName?: string | undefined; - collectionName?: string | undefined; - }; +export type ParticipantResponseType = + | 'query' + | 'schema' + | 'docs' + | 'generic' + | 'emptyRequest' + | 'askToConnect' + | 'askForNamespace'; + +interface Metadata { + intent: Exclude; + chatId: string; +} + +interface AskForNamespaceMetadata { + intent: 'askForNamespace'; + chatId: string; + databaseName?: string | undefined; + collectionName?: string | undefined; +} + +export interface ChatResult extends vscode.ChatResult { + readonly metadata: Metadata | AskForNamespaceMetadata; +} - constructor({ - databaseName, - collectionName, - history, - }: { - history: ReadonlyArray; - databaseName: string | undefined; - collectionName: string | undefined; - }) { - this.metadata = { +export function namespaceRequestChatResult({ + databaseName, + collectionName, + history, +}: { + history: ReadonlyArray; + databaseName: string | undefined; + collectionName: string | undefined; +}): ChatResult { + return { + metadata: { chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(history), intent: 'askForNamespace', databaseName, collectionName, - }; - } + }, + }; } -export class EmptyRequestChatResult implements vscode.ChatResult { - readonly metadata: { - chatId: string; - intent: 'emptyRequest'; +function createChatResult( + intent: ParticipantResponseType, + history: ReadonlyArray +): ChatResult { + return { + metadata: { + intent, + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(history), + }, }; +} - constructor( - history: ReadonlyArray - ) { - this.metadata = { - chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(history), - intent: 'emptyRequest', - }; - } +export function emptyRequestChatResult( + history: ReadonlyArray +): ChatResult { + return createChatResult('emptyRequest', history); } -export class AskToConnectChatResult implements vscode.ChatResult { - readonly metadata: { - chatId: string; - intent: 'askToConnect'; - }; +export function askToConnectChatResult( + history: ReadonlyArray +): ChatResult { + return createChatResult('askToConnect', history); +} - constructor( - history: ReadonlyArray - ) { - this.metadata = { - chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(history), - intent: 'askToConnect', - }; - } +export function genericRequestChatResult( + history: ReadonlyArray +): ChatResult { + return createChatResult('generic', history); +} + +export function queryRequestChatResult( + history: ReadonlyArray +): ChatResult { + return createChatResult('query', history); } diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 97ec53f44..5d2f324de 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -9,11 +9,14 @@ import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; import { GenericPrompt } from './prompts/generic'; +import type { ChatResult } from './constants'; import { - AskToConnectChatResult, + askToConnectChatResult, CHAT_PARTICIPANT_ID, - EmptyRequestChatResult, - NamespaceRequestChatResult, + emptyRequestChatResult, + genericRequestChatResult, + namespaceRequestChatResult, + queryRequestChatResult, } from './constants'; import { QueryPrompt } from './prompts/query'; import { COL_NAME_ID, DB_NAME_ID, NamespacePrompt } from './prompts/namespace'; @@ -22,6 +25,8 @@ import { getSimplifiedSampleDocuments } from './sampleDocuments'; import { getCopilotModel } from './model'; import { createMarkdownLink } from './markdown'; import { ChatMetadataStore } from './chatMetadata'; +import { chatResultFeedbackKindToTelemetryValue } from '../telemetry/telemetryService'; +import type TelemetryService from '../telemetry/telemetryService'; const log = createLogger('participant'); @@ -65,17 +70,21 @@ export default class ParticipantController { _connectionController: ConnectionController; _storageController: StorageController; _chatMetadataStore: ChatMetadataStore; + _telemetryService: TelemetryService; constructor({ connectionController, storageController, + telemetryService, }: { connectionController: ConnectionController; storageController: StorageController; + telemetryService: TelemetryService; }) { this._connectionController = connectionController; this._storageController = storageController; this._chatMetadataStore = new ChatMetadataStore(); + this._telemetryService = telemetryService; } createParticipant(context: vscode.ExtensionContext): vscode.ChatParticipant { @@ -94,6 +103,7 @@ export default class ParticipantController { log.info('Chat Participant Created', { participantId: this._participant?.id, }); + this._participant.onDidReceiveFeedback(this.handleUserFeedback.bind(this)); return this._participant; } @@ -168,7 +178,7 @@ export default class ParticipantController { context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken - ): Promise { + ): Promise { const messages = GenericPrompt.buildMessages({ request, context, @@ -202,13 +212,7 @@ export default class ParticipantController { }); } - return { - metadata: { - chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( - context.history - ), - }, - }; + return genericRequestChatResult(context.history); } async connectWithParticipant(id?: string): Promise { @@ -514,7 +518,7 @@ export default class ParticipantController { databaseName: string | undefined; collectionName: string | undefined; stream: vscode.ChatResponseStream; - }): Promise { + }): Promise { // If no database or collection name is found in the user prompt, // we retrieve the available namespaces from the current connection. // Users can then select a value by clicking on an item in the list. @@ -536,7 +540,7 @@ export default class ParticipantController { } } - return new NamespaceRequestChatResult({ + return namespaceRequestChatResult({ databaseName, collectionName, history: context.history, @@ -546,7 +550,7 @@ export default class ParticipantController { _askToConnect( context: vscode.ChatContext, stream: vscode.ChatResponseStream - ): vscode.ChatResult { + ): ChatResult { const tree = this.getConnectionsTree(); stream.markdown( "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against.\n\n" @@ -555,7 +559,7 @@ export default class ParticipantController { for (const item of tree) { stream.markdown(item); } - return new AskToConnectChatResult(context.history); + return askToConnectChatResult(context.history); } // The sample documents returned from this are simplified (strings and arrays shortened). @@ -615,23 +619,19 @@ export default class ParticipantController { }: { context: vscode.ChatContext; stream: vscode.ChatResponseStream; - }): Promise { + }): Promise { const lastMessageMetaData: vscode.ChatResponseTurn | undefined = context .history[context.history.length - 1] as vscode.ChatResponseTurn; - if ( - (lastMessageMetaData?.result as NamespaceRequestChatResult)?.metadata - ?.intent !== 'askForNamespace' - ) { + const lastMessage = lastMessageMetaData?.result as ChatResult; + if (lastMessage?.metadata?.intent !== 'askForNamespace') { stream.markdown(GenericPrompt.getEmptyRequestResponse()); - return new EmptyRequestChatResult(context.history); + return emptyRequestChatResult(context.history); } // When the last message was asking for a database or collection name, // we re-ask the question. let tree: vscode.MarkdownString[]; - const databaseName = ( - lastMessageMetaData.result as NamespaceRequestChatResult - ).metadata.databaseName; + const databaseName = lastMessage.metadata.databaseName; if (databaseName) { stream.markdown( vscode.l10n.t( @@ -652,7 +652,7 @@ export default class ParticipantController { stream.markdown(item); } - return new NamespaceRequestChatResult({ + return namespaceRequestChatResult({ databaseName, collectionName: undefined, history: context.history, @@ -665,7 +665,7 @@ export default class ParticipantController { context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken - ): Promise { + ): Promise { if (!this._connectionController.getActiveDataService()) { return this._askToConnect(context, stream); } @@ -744,7 +744,7 @@ export default class ParticipantController { }); } - return { metadata: {} }; + return queryRequestChatResult(context.history); } async chatHandler( @@ -754,7 +754,7 @@ export default class ParticipantController { vscode.ChatResponseStream, vscode.CancellationToken ] - ): Promise { + ): Promise { const [request, , stream] = args; if ( @@ -762,7 +762,7 @@ export default class ParticipantController { (!request.prompt || request.prompt.trim().length === 0) ) { stream.markdown(GenericPrompt.getEmptyRequestResponse()); - return new EmptyRequestChatResult(args[1].history); + return emptyRequestChatResult(args[1].history); } const hasBeenShownWelcomeMessageAlready = !!this._storageController.get( @@ -790,4 +790,12 @@ export default class ParticipantController { } return await this.handleGenericRequest(...args); } + + handleUserFeedback(feedback: vscode.ChatResultFeedback): void { + this._telemetryService.trackCopilotParticipantFeedback({ + feedback: chatResultFeedbackKindToTelemetryValue(feedback.kind), + reason: feedback.unhelpfulReason, + response_type: (feedback.result as ChatResult)?.metadata.intent, + }); + } } diff --git a/src/participant/prompts/history.ts b/src/participant/prompts/history.ts index fe13d2cd6..d14ded4bd 100644 --- a/src/participant/prompts/history.ts +++ b/src/participant/prompts/history.ts @@ -1,9 +1,5 @@ import * as vscode from 'vscode'; -import type { - AskToConnectChatResult, - EmptyRequestChatResult, - NamespaceRequestChatResult, -} from '../constants'; +import type { ChatResult, ParticipantResponseType } from '../constants'; // When passing the history to the model we only want contextual messages // to be passed. This function parses through the history and returns @@ -36,13 +32,16 @@ export function getHistoryMessages({ if (historyItem instanceof vscode.ChatResponseTurn) { let message = ''; + // Skip a response to an empty user prompt message or connect message. + const responseTypesToSkip: ParticipantResponseType[] = [ + 'emptyRequest', + 'askToConnect', + ]; if ( - (historyItem.result as EmptyRequestChatResult).metadata?.intent === - 'emptyRequest' || - (historyItem.result as AskToConnectChatResult).metadata?.intent === - 'askToConnect' + responseTypesToSkip.indexOf( + (historyItem.result as ChatResult)?.metadata.intent + ) > -1 ) { - // Skip a response to an empty user prompt message or connect message. continue; } @@ -51,8 +50,8 @@ export function getHistoryMessages({ message += fragment.value.value; if ( - (historyItem.result as NamespaceRequestChatResult).metadata - ?.intent === 'askForNamespace' + (historyItem.result as ChatResult)?.metadata.intent === + 'askForNamespace' ) { // When the message is the assistant asking for part of a namespace, // we only want to include the question asked, not the user's diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index f3bcb1bf4..3a23a1a7d 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -12,6 +12,7 @@ import { getConnectionTelemetryProperties } from './connectionTelemetry'; import type { NewConnectionTelemetryEventProperties } from './connectionTelemetry'; import type { ShellEvaluateResult } from '../types/playgroundType'; import type { StorageController } from '../storage'; +import type { ParticipantResponseType } from '../participant/constants'; const log = createLogger('telemetry'); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -93,7 +94,28 @@ type SavedConnectionsLoadedProperties = { connections_with_secrets_in_secret_storage: number; }; -export type TelemetryEventProperties = +type TelemetryFeedbackKind = 'positive' | 'negative' | undefined; + +type ParticipantFeedbackProperties = { + feedback: TelemetryFeedbackKind; + response_type: ParticipantResponseType; + reason?: String; +}; + +export function chatResultFeedbackKindToTelemetryValue( + kind: vscode.ChatResultFeedbackKind +): TelemetryFeedbackKind { + switch (kind) { + case vscode.ChatResultFeedbackKind.Helpful: + return 'positive'; + case vscode.ChatResultFeedbackKind.Unhelpful: + return 'negative'; + default: + return undefined; + } +} + +type TelemetryEventProperties = | PlaygroundTelemetryEventProperties | LinkClickedTelemetryEventProperties | ExtensionCommandRunTelemetryEventProperties @@ -107,7 +129,8 @@ export type TelemetryEventProperties = | PlaygroundLoadedTelemetryEventProperties | KeytarSecretsMigrationFailedProperties | SavedConnectionsLoadedProperties - | SurveyActionProperties; + | SurveyActionProperties + | ParticipantFeedbackProperties; export enum TelemetryEventTypes { PLAYGROUND_CODE_EXECUTED = 'Playground Code Executed', @@ -127,6 +150,7 @@ export enum TelemetryEventTypes { SAVED_CONNECTIONS_LOADED = 'Saved Connections Loaded', SURVEY_CLICKED = 'Survey link clicked', SURVEY_DISMISSED = 'Survey prompt dismissed', + PARTICIPANT_FEEDBACK = 'Participant Feedback', } /** @@ -215,7 +239,7 @@ export default class TelemetryService { return true; } - _segmentAnalyticsTrack(segmentProperties: SegmentProperties) { + _segmentAnalyticsTrack(segmentProperties: SegmentProperties): void { if (!this._isTelemetryFeatureEnabled()) { return; } @@ -378,4 +402,8 @@ export default class TelemetryService { keytarSecretsMigrationFailedProps ); } + + trackCopilotParticipantFeedback(props: ParticipantFeedbackProperties): void { + this.track(TelemetryEventTypes.PARTICIPANT_FEEDBACK, props); + } } diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 378979119..f70c1d924 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { beforeEach, afterEach } from 'mocha'; import { expect } from 'chai'; +import type { SinonSpy } from 'sinon'; import sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; import { ObjectId, Int32 } from 'bson'; @@ -50,10 +51,14 @@ suite('Participant Controller Test Suite', function () { let chatTokenStub; let countTokensStub; let sendRequestStub; + let telemetryTrackStub: SinonSpy; beforeEach(function () { testStorageController = new StorageController(extensionContextStub); testStatusView = new StatusView(extensionContextStub); + + telemetryTrackStub = sinon.stub(); + testTelemetryService = new TelemetryService( testStorageController, extensionContextStub @@ -66,6 +71,7 @@ suite('Participant Controller Test Suite', function () { testParticipantController = new ParticipantController({ connectionController: testConnectionController, storageController: testStorageController, + telemetryService: testTelemetryService, }); chatContextStub = { history: [ @@ -109,6 +115,8 @@ suite('Participant Controller Test Suite', function () { }, ]) ); + + sinon.replace(testTelemetryService, 'track', telemetryTrackStub); }); afterEach(function () { @@ -1092,4 +1100,61 @@ suite('Participant Controller Test Suite', function () { }); }); }); + + suite('telemetry', function () { + test('reports positive user feedback', function () { + testParticipantController.handleUserFeedback({ + kind: vscode.ChatResultFeedbackKind.Helpful, + result: { + metadata: { + intent: 'askToConnect', + responseContent: '```creditCardNumber: 1234-5678-9012-3456```', + }, + }, + }); + + sinon.assert.calledOnce(telemetryTrackStub); + expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( + 'Participant Feedback' + ); + + const properties = telemetryTrackStub.lastCall.args[1]; + expect(properties.feedback).to.be.equal('positive'); + expect(properties.reason).to.be.undefined; + expect(properties.response_type).to.be.equal('askToConnect'); + + // Ensure we're not leaking the response content into the telemetry payload + expect(JSON.stringify(properties)) + .to.not.include('creditCardNumber') + .and.not.include('1234-5678-9012-3456'); + }); + + test('reports negative user feedback', function () { + testParticipantController.handleUserFeedback({ + kind: vscode.ChatResultFeedbackKind.Unhelpful, + result: { + metadata: { + intent: 'query', + responseContent: 'SSN: 123456789', + }, + }, + unhelpfulReason: 'incompleteCode', + }); + + sinon.assert.calledOnce(telemetryTrackStub); + expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( + 'Participant Feedback' + ); + + const properties = telemetryTrackStub.lastCall.args[1]; + expect(properties.feedback).to.be.equal('negative'); + expect(properties.reason).to.be.equal('incompleteCode'); + expect(properties.response_type).to.be.equal('query'); + + // Ensure we're not leaking the response content into the telemetry payload + expect(JSON.stringify(properties)) + .to.not.include('SSN') + .and.not.include('123456789'); + }); + }); }); diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index 42f1c1659..0055f0e50 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -14,6 +14,7 @@ import { DocumentSource } from '../../../documentSource'; import { mdbTestExtension } from '../stubbableMdbExtension'; import { DatabaseTreeItem, DocumentTreeItem } from '../../../explorer'; import { DataServiceStub } from '../stubs'; +import { chatResultFeedbackKindToTelemetryValue } from '../../../telemetry/telemetryService'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require('../../../../package.json'); @@ -750,4 +751,22 @@ suite('Telemetry Controller Test Suite', () => { }) ); }); + + function enumKeys< + TEnum extends object, + TKey extends keyof TEnum = keyof TEnum + >(obj: TEnum): TKey[] { + return Object.keys(obj).filter((k) => Number.isNaN(k)) as TKey[]; + } + + test('ChatResultFeedbackKind to TelemetryFeedbackKind maps all values', () => { + for (const kind of enumKeys(vscode.ChatResultFeedbackKind)) { + expect( + chatResultFeedbackKindToTelemetryValue( + vscode.ChatResultFeedbackKind[kind] + ), + `Expect ${kind} to produce a concrete telemetry value` + ).to.not.be.undefined; + } + }); }); diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts new file mode 100644 index 000000000..315fb76ed --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -0,0 +1,468 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + export interface ChatParticipant { + onDidPerformAction: Event; + } + + /** + * Now only used for the "intent detection" API below + */ + export interface ChatCommand { + readonly name: string; + readonly description: string; + } + + export class ChatResponseDetectedParticipantPart { + participant: string; + // TODO@API validate this against statically-declared slash commands? + command?: ChatCommand; + constructor(participant: string, command?: ChatCommand); + } + + export interface ChatVulnerability { + title: string; + description: string; + // id: string; // Later we will need to be able to link these across multiple content chunks. + } + + export class ChatResponseMarkdownWithVulnerabilitiesPart { + value: MarkdownString; + vulnerabilities: ChatVulnerability[]; + constructor( + value: string | MarkdownString, + vulnerabilities: ChatVulnerability[] + ); + } + + /** + * Displays a {@link Command command} as a button in the chat response. + */ + export interface ChatCommandButton { + command: Command; + } + + export interface ChatDocumentContext { + uri: Uri; + version: number; + ranges: Range[]; + } + + export class ChatResponseTextEditPart { + uri: Uri; + edits: TextEdit[]; + constructor(uri: Uri, edits: TextEdit | TextEdit[]); + } + + export class ChatResponseConfirmationPart { + title: string; + message: string; + data: any; + buttons?: string[]; + constructor(title: string, message: string, data: any, buttons?: string[]); + } + + export class ChatResponseCodeCitationPart { + value: Uri; + license: string; + snippet: string; + constructor(value: Uri, license: string, snippet: string); + } + + export type ExtendedChatResponsePart = + | ChatResponsePart + | ChatResponseTextEditPart + | ChatResponseDetectedParticipantPart + | ChatResponseConfirmationPart + | ChatResponseCodeCitationPart + | ChatResponseReferencePart2 + | ChatResponseMovePart; + + export class ChatResponseWarningPart { + value: MarkdownString; + constructor(value: string | MarkdownString); + } + + export class ChatResponseProgressPart2 extends ChatResponseProgressPart { + value: string; + task?: ( + progress: Progress + ) => Thenable; + constructor( + value: string, + task?: ( + progress: Progress + ) => Thenable + ); + } + + export class ChatResponseReferencePart2 { + /** + * The reference target. + */ + value: + | Uri + | Location + | { variableName: string; value?: Uri | Location } + | string; + + /** + * The icon for the reference. + */ + iconPath?: + | Uri + | ThemeIcon + | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }; + options?: { + status?: { + description: string; + kind: ChatResponseReferencePartStatusKind; + }; + }; + + /** + * Create a new ChatResponseReferencePart. + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + constructor( + value: + | Uri + | Location + | { variableName: string; value?: Uri | Location } + | string, + iconPath?: + | Uri + | ThemeIcon + | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }, + options?: { + status?: { + description: string; + kind: ChatResponseReferencePartStatusKind; + }; + } + ); + } + + export class ChatResponseMovePart { + readonly uri: Uri; + readonly range: Range; + + constructor(uri: Uri, range: Range); + } + + // Extended to add `SymbolInformation`. Would also be added to `constructor`. + export interface ChatResponseAnchorPart { + /** + * The target of this anchor. + * + * If this is a {@linkcode Uri} or {@linkcode Location}, this is rendered as a normal link. + * + * If this is a {@linkcode SymbolInformation}, this is rendered as a symbol link. + */ + value2: Uri | Location | SymbolInformation; + } + + export interface ChatResponseStream { + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + * @param task If provided, a task to run while the progress is displayed. When the Thenable resolves, the progress will be marked complete in the UI, and the progress message will be updated to the resolved string if one is specified. + * @returns This stream. + */ + progress( + value: string, + task?: ( + progress: Progress + ) => Thenable + ): void; + + textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; + markdownWithVulnerabilities( + value: string | MarkdownString, + vulnerabilities: ChatVulnerability[] + ): void; + detectedParticipant(participant: string, command?: ChatCommand): void; + push( + part: + | ChatResponsePart + | ChatResponseTextEditPart + | ChatResponseDetectedParticipantPart + | ChatResponseWarningPart + | ChatResponseProgressPart2 + ): void; + + /** + * Show an inline message in the chat view asking the user to confirm an action. + * Multiple confirmations may be shown per response. The UI might show "Accept All" / "Reject All" actions. + * @param title The title of the confirmation entry + * @param message An extra message to display to the user + * @param data An arbitrary JSON-stringifiable object that will be included in the ChatRequest when + * the confirmation is accepted or rejected + * TODO@API should this be MarkdownString? + * TODO@API should actually be a more generic function that takes an array of buttons + */ + confirmation( + title: string, + message: string, + data: any, + buttons?: string[] + ): void; + + /** + * Push a warning to this stream. Short-hand for + * `push(new ChatResponseWarningPart(message))`. + * + * @param message A warning message + * @returns This stream. + */ + warning(message: string | MarkdownString): void; + + reference( + value: Uri | Location | { variableName: string; value?: Uri | Location }, + iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri } + ): void; + + reference2( + value: + | Uri + | Location + | string + | { variableName: string; value?: Uri | Location }, + iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }, + options?: { + status?: { + description: string; + kind: ChatResponseReferencePartStatusKind; + }; + } + ): void; + + codeCitation(value: Uri, license: string, snippet: string): void; + + push(part: ExtendedChatResponsePart): void; + } + + export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3, + } + + /** + * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? + * Does it show up in history? + */ + export interface ChatRequest { + /** + * The `data` for any confirmations that were accepted + */ + acceptedConfirmationData?: any[]; + + /** + * The `data` for any confirmations that were rejected + */ + rejectedConfirmationData?: any[]; + } + + // TODO@API fit this into the stream + export interface ChatUsedContext { + documents: ChatDocumentContext[]; + } + + export interface ChatParticipant { + /** + * Provide a set of variables that can only be used with this participant. + */ + participantVariableProvider?: { + provider: ChatParticipantCompletionItemProvider; + triggerCharacters: string[]; + }; + } + + export interface ChatParticipantCompletionItemProvider { + provideCompletionItems( + query: string, + token: CancellationToken + ): ProviderResult; + } + + export class ChatCompletionItem { + id: string; + label: string | CompletionItemLabel; + values: ChatVariableValue[]; + fullName?: string; + icon?: ThemeIcon; + insertText?: string; + detail?: string; + documentation?: string | MarkdownString; + command?: Command; + + constructor( + id: string, + label: string | CompletionItemLabel, + values: ChatVariableValue[] + ); + } + + export type ChatExtendedRequestHandler = ( + request: ChatRequest, + context: ChatContext, + response: ChatResponseStream, + token: CancellationToken + ) => ProviderResult; + + export interface ChatResult { + nextQuestion?: { + prompt: string; + participant?: string; + command?: string; + }; + } + + export namespace chat { + /** + * Create a chat participant with the extended progress type + */ + export function createChatParticipant( + id: string, + handler: ChatExtendedRequestHandler + ): ChatParticipant; + + export function registerChatParticipantDetectionProvider( + participantDetectionProvider: ChatParticipantDetectionProvider + ): Disposable; + } + + export interface ChatParticipantMetadata { + participant: string; + command?: string; + disambiguation: { + category: string; + description: string; + examples: string[]; + }[]; + } + + export interface ChatParticipantDetectionResult { + participant: string; + command?: string; + } + + export interface ChatParticipantDetectionProvider { + provideParticipantDetection( + chatRequest: ChatRequest, + context: ChatContext, + options: { + participants?: ChatParticipantMetadata[]; + location: ChatLocation; + }, + token: CancellationToken + ): ProviderResult; + } + + /* + * User action events + */ + + export enum ChatCopyKind { + // Keyboard shortcut or context menu + Action = 1, + Toolbar = 2, + } + + export interface ChatCopyAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'copy'; + codeBlockIndex: number; + copyKind: ChatCopyKind; + copiedCharacters: number; + totalCharacters: number; + copiedText: string; + } + + export interface ChatInsertAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'insert'; + codeBlockIndex: number; + totalCharacters: number; + newFile?: boolean; + userAction?: string; + codeMapper?: string; + } + + export interface ChatTerminalAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'runInTerminal'; + codeBlockIndex: number; + languageId?: string; + } + + export interface ChatCommandAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'command'; + commandButton: ChatCommandButton; + } + + export interface ChatFollowupAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'followUp'; + followup: ChatFollowup; + } + + export interface ChatBugReportAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'bug'; + } + + export interface ChatEditorAction { + kind: 'editor'; + accepted: boolean; + } + + export interface ChatUserActionEvent { + readonly result: ChatResult; + readonly action: + | ChatCopyAction + | ChatInsertAction + | ChatTerminalAction + | ChatCommandAction + | ChatFollowupAction + | ChatBugReportAction + | ChatEditorAction; + } + + export interface ChatPromptReference { + /** + * TODO Needed for now to drive the variableName-type reference, but probably both of these should go away in the future. + */ + readonly name: string; + } + + export interface ChatResultFeedback { + readonly unhelpfulReason?: string; + } +} From b37272fe69f7b29ba9f71fb2d1ebe6c3c1e4bebe Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 19 Sep 2024 11:48:17 +0200 Subject: [PATCH 23/45] feat(participant): Report telemetry for welcome VSCODE-602 (#821) --- src/participant/participant.ts | 12 +- src/telemetry/telemetryService.ts | 1 + .../suite/participant/participant.test.ts | 537 ++++++++---------- 3 files changed, 258 insertions(+), 292 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 5d2f324de..bd2f92580 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -25,7 +25,10 @@ import { getSimplifiedSampleDocuments } from './sampleDocuments'; import { getCopilotModel } from './model'; import { createMarkdownLink } from './markdown'; import { ChatMetadataStore } from './chatMetadata'; -import { chatResultFeedbackKindToTelemetryValue } from '../telemetry/telemetryService'; +import { + chatResultFeedbackKindToTelemetryValue, + TelemetryEventTypes, +} from '../telemetry/telemetryService'; import type TelemetryService from '../telemetry/telemetryService'; const log = createLogger('participant'); @@ -775,7 +778,12 @@ export default class ParticipantController { Interact with your MongoDB clusters and generate MongoDB-related code more efficiently with intelligent AI-powered feature, available today in the MongoDB extension.\n\n Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more information.\n\n`) ); - void this._storageController.update( + + this._telemetryService.track( + TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN + ); + + await this._storageController.update( StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE, true ); diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 3a23a1a7d..f7ff48731 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -151,6 +151,7 @@ export enum TelemetryEventTypes { SURVEY_CLICKED = 'Survey link clicked', SURVEY_DISMISSED = 'Survey prompt dismissed', PARTICIPANT_FEEDBACK = 'Participant Feedback', + PARTICIPANT_WELCOME_SHOWN = 'Participant Welcome Shown', } /** diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index f70c1d924..7ea3849c3 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -14,8 +14,11 @@ import ConnectionController from '../../../connectionController'; import { StorageController } from '../../../storage'; import { StatusView } from '../../../views'; import { ExtensionContextStub } from '../stubs'; -import TelemetryService from '../../../telemetry/telemetryService'; +import TelemetryService, { + TelemetryEventTypes, +} from '../../../telemetry/telemetryService'; import { TEST_DATABASE_URI } from '../dbTestHelper'; +import type { ChatResult } from '../../../participant/constants'; import { CHAT_PARTICIPANT_ID } from '../../../participant/constants'; import { SecretStorageLocation, @@ -46,13 +49,26 @@ suite('Participant Controller Test Suite', function () { let testStatusView: StatusView; let testTelemetryService: TelemetryService; let testParticipantController: ParticipantController; - let chatContextStub; - let chatStreamStub; + let chatContextStub: vscode.ChatContext; + let chatStreamStub: { + markdown: sinon.SinonSpy; + button: sinon.SinonSpy; + }; let chatTokenStub; let countTokensStub; let sendRequestStub; let telemetryTrackStub: SinonSpy; + const invokeChatHandler = async ( + request: vscode.ChatRequest + ): Promise => + testParticipantController.chatHandler( + request, + chatContextStub, + chatStreamStub as unknown as vscode.ChatResponseStream, + chatTokenStub + ); + beforeEach(function () { testStorageController = new StorageController(extensionContextStub); testStatusView = new StatusView(extensionContextStub); @@ -78,7 +94,8 @@ suite('Participant Controller Test Suite', function () { { participant: CHAT_PARTICIPANT_ID, prompt: 'hi', - response: 'hello', + response: [new vscode.ChatResponseMarkdownPart('hello')], + result: {}, }, ], }; @@ -182,12 +199,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - const chatResult = await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + const chatResult = await invokeChatHandler(chatRequestMock); const connectMessage = chatStreamStub.markdown.getCall(0).args[0]; expect(connectMessage).to.include( "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against." @@ -225,12 +237,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - const chatResult = await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + const chatResult = await invokeChatHandler(chatRequestMock); const connectMessage = chatStreamStub.markdown.getCall(0).args[0]; expect(connectMessage).to.include( "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against." @@ -261,20 +268,10 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - const chatResult = await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + const chatResult = await invokeChatHandler(chatRequestMock); chatRequestMock.prompt = ''; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); const emptyMessage = chatStreamStub.markdown.getCall(3).args[0]; expect(emptyMessage).to.include( @@ -372,14 +369,22 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); const welcomeMessage = chatStreamStub.markdown.firstCall.args[0]; expect(welcomeMessage).to.include('Welcome to MongoDB Participant!'); + + sinon.assert.calledOnce(telemetryTrackStub); + expect(telemetryTrackStub.lastCall.args[0]).to.equal( + TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN + ); + expect(telemetryTrackStub.lastCall.args[1]).to.be.undefined; + + telemetryTrackStub + .getCalls() + .map((call) => call.args[0]) + .filter( + (arg) => arg === TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN + ).length; }); }); @@ -392,6 +397,29 @@ suite('Participant Controller Test Suite', function () { ); }); + afterEach(function () { + // Ensure welcome message is not shown again + const welcomeMessages = chatStreamStub.markdown + .getCalls() + .map((call) => call.args[0]) + .filter( + (message) => + typeof message === 'string' && + message.includes('Welcome to MongoDB Participant!') + ); + expect(welcomeMessages).to.be.empty; + + // Ensure we haven't reported the welcome screen to telemetry + const telemetryEvents = telemetryTrackStub + .getCalls() + .map((call) => call.args[0]) + .filter( + (arg) => arg === TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN + ); + + expect(telemetryEvents).to.be.empty; + }); + suite('generic command', function () { test('generates a query', async function () { const chatRequestMock = { @@ -399,12 +427,7 @@ suite('Participant Controller Test Suite', function () { command: undefined, references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ command: 'mdb.runParticipantQuery', @@ -433,12 +456,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ command: 'mdb.runParticipantQuery', title: '▶️ Run', @@ -467,12 +485,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.include( 'Collection schema: _id: ObjectId\n' + @@ -521,12 +534,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.include( 'Sample documents: [\n' + @@ -579,12 +587,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.include( 'Sample document: {\n' + @@ -631,12 +634,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.include( 'Sample document: {\n' + @@ -680,12 +678,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.not.include('Sample documents'); }); @@ -698,12 +691,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; expect(messages[0].content).to.not.include('Sample documents'); }); @@ -717,12 +705,7 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - const chatResult = await testParticipantController.chatHandler( - chatRequestMock, - chatContextStub, - chatStreamStub, - chatTokenStub - ); + const chatResult = await invokeChatHandler(chatRequestMock); const askForDBMessage = chatStreamStub.markdown.getCall(0).args[0]; expect(askForDBMessage).to.include( 'What is the name of the database you would like this query to run against?' @@ -757,42 +740,40 @@ suite('Participant Controller Test Suite', function () { sendRequestStub.onCall(1).resolves({ text: ['DATABASE_NAME: dbOne\n'], }); - const chatResult2 = await testParticipantController.chatHandler( - chatRequestMock, - { - history: [ + + chatContextStub = { + history: [ + { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + } as vscode.ChatRequestTurn, + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), { - prompt: 'find all docs by a name example', - command: 'query', - references: [], participant: CHAT_PARTICIPANT_ID, - } as vscode.ChatRequestTurn, - Object.assign( - Object.create(vscode.ChatResponseTurn.prototype), - { - participant: CHAT_PARTICIPANT_ID, - response: [ - { - value: { - value: - 'What is the name of the database you would like this query to run against?', - } as vscode.MarkdownString, - }, - ], - command: 'query', - result: { - metadata: { - intent: 'askForNamespace', - chatId: firstChatId, - }, + response: [ + { + value: { + value: + 'What is the name of the database you would like this query to run against?', + } as vscode.MarkdownString, }, - } as vscode.ChatResponseTurn - ), - ], - }, - chatStreamStub, - chatTokenStub - ); + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', + chatId: firstChatId, + }, + }, + } as vscode.ChatResponseTurn + ), + ], + }; + + const chatResult2 = await invokeChatHandler(chatRequestMock); const askForCollMessage = chatStreamStub.markdown.getCall(12).args[0]; @@ -829,76 +810,66 @@ suite('Participant Controller Test Suite', function () { sendRequestStub.onCall(2).resolves({ text: ['DATABASE_NAME: dbOne\n', 'COLLECTION_NAME: collOne\n`'], }); - await testParticipantController.chatHandler( - chatRequestMock, - { - history: [ - Object.assign( - Object.create(vscode.ChatRequestTurn.prototype), - { - prompt: 'find all docs by a name example', - command: 'query', - references: [], - participant: CHAT_PARTICIPANT_ID, - } - ), - Object.assign( - Object.create(vscode.ChatResponseTurn.prototype), - { - participant: CHAT_PARTICIPANT_ID, - response: [ - { - value: { - value: - 'Which database would you like to query within this database?', - } as vscode.MarkdownString, - }, - ], - command: 'query', - result: { - metadata: { - intent: 'askForNamespace', - }, + chatContextStub = { + history: [ + Object.assign(Object.create(vscode.ChatRequestTurn.prototype), { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + }), + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), + { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: + 'Which database would you like to query within this database?', + } as vscode.MarkdownString, }, - } - ), - Object.assign( - Object.create(vscode.ChatRequestTurn.prototype), - { - prompt: 'dbOne', - command: 'query', - references: [], - participant: CHAT_PARTICIPANT_ID, - } - ), - Object.assign( - Object.create(vscode.ChatResponseTurn.prototype), - { - participant: CHAT_PARTICIPANT_ID, - response: [ - { - value: { - value: - 'Which collection would you like to query within dbOne?', - } as vscode.MarkdownString, - }, - ], - command: 'query', - result: { - metadata: { - intent: 'askForNamespace', - databaseName: 'dbOne', - collectionName: 'collOne', - chatId: firstChatId, - }, + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', }, - } - ), - ], - }, - chatStreamStub, - chatTokenStub - ); + }, + } + ), + Object.assign(Object.create(vscode.ChatRequestTurn.prototype), { + prompt: 'dbOne', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + }), + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), + { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: + 'Which collection would you like to query within dbOne?', + } as vscode.MarkdownString, + }, + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', + databaseName: 'dbOne', + collectionName: 'collOne', + chatId: firstChatId, + }, + }, + } + ), + ], + }; + await invokeChatHandler(chatRequestMock); expect(chatStreamStub?.button.callCount).to.equal(2); expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ @@ -929,42 +900,38 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - const chatResult = await testParticipantController.chatHandler( - chatRequestMock, - { - history: [ + chatContextStub = { + history: [ + { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + } as vscode.ChatRequestTurn, + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), { - prompt: 'find all docs by a name example', - command: 'query', - references: [], participant: CHAT_PARTICIPANT_ID, - } as vscode.ChatRequestTurn, - Object.assign( - Object.create(vscode.ChatResponseTurn.prototype), - { - participant: CHAT_PARTICIPANT_ID, - response: [ - { - value: { - value: - 'What is the name of the database you would like this query to run against?', - } as vscode.MarkdownString, - }, - ], - command: 'query', - result: { - metadata: { - intent: 'askForNamespace', - chatId: 'pineapple', - }, + response: [ + { + value: { + value: + 'What is the name of the database you would like this query to run against?', + } as vscode.MarkdownString, }, - } as vscode.ChatResponseTurn - ), - ], - }, - chatStreamStub, - chatTokenStub - ); + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', + chatId: 'pineapple', + }, + }, + } as vscode.ChatResponseTurn + ), + ], + }; + const chatResult = await invokeChatHandler(chatRequestMock); const emptyMessage = chatStreamStub.markdown.getCall(0).args[0]; expect(emptyMessage).to.include( @@ -999,76 +966,66 @@ suite('Participant Controller Test Suite', function () { command: 'query', references: [], }; - const chatResult = await testParticipantController.chatHandler( - chatRequestMock, - { - history: [ - Object.assign( - Object.create(vscode.ChatRequestTurn.prototype), - { - prompt: 'find all docs by a name example', - command: 'query', - references: [], - participant: CHAT_PARTICIPANT_ID, - } - ), - Object.assign( - Object.create(vscode.ChatResponseTurn.prototype), - { - participant: CHAT_PARTICIPANT_ID, - response: [ - { - value: { - value: - 'Which database would you like to query within this database?', - } as vscode.MarkdownString, - }, - ], - command: 'query', - result: { - metadata: { - intent: 'askForNamespace', - }, + chatContextStub = { + history: [ + Object.assign(Object.create(vscode.ChatRequestTurn.prototype), { + prompt: 'find all docs by a name example', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + }), + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), + { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: + 'Which database would you like to query within this database?', + } as vscode.MarkdownString, }, - } - ), - Object.assign( - Object.create(vscode.ChatRequestTurn.prototype), - { - prompt: 'dbOne', - command: 'query', - references: [], - participant: CHAT_PARTICIPANT_ID, - } - ), - Object.assign( - Object.create(vscode.ChatResponseTurn.prototype), - { - participant: CHAT_PARTICIPANT_ID, - response: [ - { - value: { - value: - 'Which collection would you like to query within dbOne?', - } as vscode.MarkdownString, - }, - ], - command: 'query', - result: { - metadata: { - intent: 'askForNamespace', - databaseName: 'dbOne', - collectionName: undefined, - chatId: 'pineapple', - }, + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', }, - } - ), - ], - }, - chatStreamStub, - chatTokenStub - ); + }, + } + ), + Object.assign(Object.create(vscode.ChatRequestTurn.prototype), { + prompt: 'dbOne', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + }), + Object.assign( + Object.create(vscode.ChatResponseTurn.prototype), + { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: + 'Which collection would you like to query within dbOne?', + } as vscode.MarkdownString, + }, + ], + command: 'query', + result: { + metadata: { + intent: 'askForNamespace', + databaseName: 'dbOne', + collectionName: undefined, + chatId: 'pineapple', + }, + }, + } + ), + ], + }; + const chatResult = await invokeChatHandler(chatRequestMock); const emptyMessage = chatStreamStub.markdown.getCall(0).args[0]; expect(emptyMessage).to.include( From e8b91be628a966ffd42f5b3e91f630cd98278359 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Thu, 19 Sep 2024 18:10:33 +0200 Subject: [PATCH 24/45] feat(participant): implement the docs command VSCODE-570 (#817) * feat(copilot): implement the /docs command VSCODE-570 * feat: use conversationId from history * feat: use version in user-agent * test: add tests for DocsChatbotAIService * refactor: fix type * refactor: update messages * refactor: cleanup * feat: fall back to copilot when chatbot is not available * test: add fallback test * build: add docs chatbot env variable * refactor: update log message * chore: move mongodb-rag-core to dev deps * refactor: address pr comments * test: add test that uses chatbot --- .github/workflows/draft-release.yaml | 1 + .github/workflows/test-and-build.yaml | 1 + package-lock.json | 829 +++++++++++++++++- package.json | 10 +- scripts/generate-constants.ts | 31 + scripts/generate-keyfile.ts | 28 - src/mdbExtensionController.ts | 1 + src/participant/chatMetadata.ts | 1 + src/participant/constants.ts | 9 + src/participant/docsChatbotAIService.ts | 130 +++ src/participant/markdown.ts | 8 +- src/participant/participant.ts | 244 +++++- src/telemetry/telemetryService.ts | 21 +- .../participant/docsChatbotAIService.test.ts | 108 +++ .../suite/participant/participant.test.ts | 56 ++ 15 files changed, 1372 insertions(+), 106 deletions(-) create mode 100644 scripts/generate-constants.ts delete mode 100644 scripts/generate-keyfile.ts create mode 100644 src/participant/docsChatbotAIService.ts create mode 100644 src/test/suite/participant/docsChatbotAIService.test.ts diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index 2246bc3ac..62ec8fb49 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -80,6 +80,7 @@ jobs: uses: ./.github/workflows/actions/test-and-build with: SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_PROD }} + MONGODB_DOCS_CHATBOT_BASE_URI: ${{ secrets.MONGODB_DOCS_CHATBOT_BASE_URI_PROD }} ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index ceaaa04f3..73fc6ef88 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -36,6 +36,7 @@ jobs: uses: ./.github/workflows/actions/test-and-build with: SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_PROD }} + MONGODB_DOCS_CHATBOT_BASE_URI: ${{ secrets.MONGODB_DOCS_CHATBOT_BASE_URI_PROD }} ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} diff --git a/package-lock.json b/package-lock.json index dbf2cddcf..2ffe1c822 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "mocha-junit-reporter": "^2.2.1", "mocha-multi": "^1.1.7", "mongodb-client-encryption": "^6.0.1", + "mongodb-rag-core": "^0.4.1", "mongodb-runner": "^5.6.4", "node-fetch": "^2.7.0", "node-loader": "^0.6.0", @@ -145,6 +146,30 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.25.2.tgz", + "integrity": "sha512-F1Hck/asswwidFLtGdMg3XYgRxEUfygNbpkq5KEaEGsHNaSfxeX18/uZGQCL0oQNcj/tYNx8BaFXVwRhFDi45g==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.50", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", + "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@asamuzakjp/dom-selector": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz", @@ -856,6 +881,35 @@ } } }, + "node_modules/@azure-rest/core-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-1.4.0.tgz", + "integrity": "sha512-ozTDPBVUDR5eOnMIwhggbnVmOrka4fXCs8n8mvUo4WLLc38kki6bAOByDoVZZPz/pZy2jMt2kwfpvy/UjALj6w==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.5.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure-rest/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -968,6 +1022,18 @@ "node": ">= 14" } }, + "node_modules/@azure/core-sse": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@azure/core-sse/-/core-sse-2.1.3.tgz", + "integrity": "sha512-KSSdIKy8kvWCpYr8Hzpu22j3wcXsVTYE0IlgmI1T/aHvBDsLgV91y90UTfVWnuiuApRLCCVC4gS09ApBGOmYQA==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@azure/core-tracing": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", @@ -1103,6 +1169,24 @@ "node": ">=16" } }, + "node_modules/@azure/openai": { + "version": "1.0.0-beta.12", + "resolved": "https://registry.npmjs.org/@azure/openai/-/openai-1.0.0-beta.12.tgz", + "integrity": "sha512-qKblxr6oVa8GsyNzY+/Ub9VmEsPYKhBrUrPaNEQiM+qrxnBPVm9kaeqGFFb/U78Q2zOabmhF9ctYt3xBW0nWnQ==", + "dev": true, + "dependencies": { + "@azure-rest/core-client": "^1.1.7", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.13.0", + "@azure/core-sse": "^2.0.0", + "@azure/core-util": "^1.4.0", + "@azure/logger": "^1.0.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -1756,6 +1840,15 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1778,6 +1871,17 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dev": true, + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2263,6 +2367,130 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langchain/anthropic": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.2.18.tgz", + "integrity": "sha512-4ZDTxMwGKTPRAi2Supu/faBSmwPIm/ga5QlazyO78Mf/8QbDR2DcvM5394FAy+X/nRAfnMbyXteO5IRJm653gw==", + "dev": true, + "dependencies": { + "@anthropic-ai/sdk": "^0.25.2", + "@langchain/core": ">=0.2.21 <0.3.0", + "fast-xml-parser": "^4.4.1", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/core": { + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.32.tgz", + "integrity": "sha512-S27M+9Qou2qtcLfFGEvANkJ/zHq5XApeQsR6Q4I7C6v9x07eoYr558h6vVy6WQmKcksgbCIJ854ikwp173wBjA==", + "dev": true, + "dependencies": { + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.1.43", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/core/node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, + "node_modules/@langchain/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@langchain/core/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@langchain/core/node_modules/langsmith": { + "version": "0.1.55", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.55.tgz", + "integrity": "sha512-6NVtI04UUnIY59I/imOX02FG/QMGfqStu8tiJtyyreKMv2GAN0EE9Z5Ap1wzOe6v8ukEcV3NwEO2LYOPwup1PQ==", + "dev": true, + "dependencies": { + "@types/uuid": "^10.0.0", + "commander": "^10.0.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@langchain/core": "*", + "langchain": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "langchain": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/openai": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.2.10.tgz", + "integrity": "sha512-ph5sYDAmhP55Fs3TW3+LXiqF+r/5zaaNO2tur9p2Otr8KWNDSgp5ezfPki1WWfuUJVoSQ+6HDYtr6n2V5N1Lew==", + "dev": true, + "dependencies": { + "@langchain/core": ">=0.2.26 <0.3.0", + "js-tiktoken": "^1.0.12", + "openai": "^4.57.3", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@leafygreen-ui/a11y": { "version": "1.4.13", "resolved": "https://registry.npmjs.org/@leafygreen-ui/a11y/-/a11y-1.4.13.tgz", @@ -5823,6 +6051,12 @@ "@types/node": "*" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -5877,6 +6111,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", @@ -7138,6 +7378,12 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -7160,6 +7406,17 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/azure-devops-node-api": { "version": "12.5.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", @@ -7813,6 +8070,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001651", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", @@ -8120,6 +8389,16 @@ "node": ">=16" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -8136,6 +8415,31 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -8151,6 +8455,16 @@ "node": ">=0.1.90" } }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dev": true, + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -8172,6 +8486,15 @@ "node": ">=14" } }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/compare-module-exports": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/compare-module-exports/-/compare-module-exports-2.1.0.tgz", @@ -9107,18 +9430,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/depcheck/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/depcheck/node_modules/minimatch": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", @@ -9614,6 +9925,12 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "devOptional": true }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -10870,6 +11187,12 @@ "node": ">=0.10.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -10924,6 +11247,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -11069,6 +11404,12 @@ "pend": "~1.2.0" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -11242,6 +11583,12 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true + }, "node_modules/focus-trap": { "version": "6.9.4", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz", @@ -11264,6 +11611,26 @@ "react-dom": ">=16.3.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -11477,6 +11844,15 @@ "node": ">= 0.6" } }, + "node_modules/front-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", + "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -11919,6 +12295,21 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -12920,6 +13311,15 @@ "resolved": "https://registry.npmjs.org/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz", "integrity": "sha512-pRlQnpaCFhDVPtkXkP+g9Ybv/CjbiQDjnKFQTEjpBfDKeV6dRDBczuFRDpM6DVfk2EjpMS8t5kwE5jPnqYl3zA==" }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -13274,6 +13674,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-tiktoken": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.14.tgz", + "integrity": "sha512-Pk3l3WOgM9joguZY2k52+jH82RtABRgB5RdGFZNUGbOKGMVlNmafcPA3b0ITcCZPu1L9UclP1tne6aw7ZI4Myg==", + "dev": true, + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13614,6 +14023,12 @@ "node": ">=0.10.0" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -13837,6 +14252,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/logform": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "dev": true, + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -14704,6 +15136,65 @@ "lodash": "^4.17.21" } }, + "node_modules/mongodb-rag-core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mongodb-rag-core/-/mongodb-rag-core-0.4.1.tgz", + "integrity": "sha512-McgQyKeex2e2qR8PNo4zmn5XEAnPs5jmdtRZn5f9GAY1tQGjN+JS7ePYgLKwsEDUxRsR56rrtn9ExJgdFo9gFg==", + "dev": true, + "dependencies": { + "@azure/openai": "^1.0.0-beta.5", + "@langchain/anthropic": "^0.2.15", + "@langchain/core": "^0.2.27", + "@langchain/openai": "^0.2.7", + "common-tags": "^1", + "dotenv": "^16.3.1", + "exponential-backoff": "^3.1.1", + "front-matter": "^4.0.2", + "gray-matter": "^4.0.3", + "mongodb": "^6.3.0", + "openai": "^3", + "toml": "^3.0.0", + "typechat": "^0.0.10", + "winston": "^3", + "yaml": "^2.3.1", + "zod": "^3.21.4" + }, + "engines": { + "node": ">=18", + "npm": ">=8" + } + }, + "node_modules/mongodb-rag-core/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/mongodb-rag-core/node_modules/openai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", + "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "dev": true, + "dependencies": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + } + }, + "node_modules/mongodb-rag-core/node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/mongodb-redact": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-1.1.3.tgz", @@ -14900,6 +15391,15 @@ "object-assign": "^4.1.0" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -15579,6 +16079,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -15611,14 +16120,14 @@ } }, "node_modules/openai": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.57.0.tgz", - "integrity": "sha512-JnwBSIYqiZ3jYjB5f2in8hQ0PRA092c6m+/6dYB0MzK0BEbn+0dioxZsPLBm5idJbg9xzLNOiGVm2OSuhZ+BdQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.59.0.tgz", + "integrity": "sha512-3bn7FypMt2R1ZDuO0+GcXgBEnVFhIzrpUkb47pQRoYvyfdZ2fQXcuP14aOc4C8F9FvCtZ/ElzJmVzVqnP4nHNg==", "dev": true, "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", - "@types/qs": "^6.9.7", + "@types/qs": "^6.9.15", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", @@ -15830,6 +16339,15 @@ "node": ">=8" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -15857,6 +16375,47 @@ "node": ">=8" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -16457,6 +17016,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -17091,6 +17656,15 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -17271,6 +17845,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -17324,6 +17907,19 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/seek-bzip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", @@ -17349,9 +17945,9 @@ "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -17600,6 +18196,21 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + }, "node_modules/sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -17937,6 +18548,15 @@ "nan": "^2.18.0" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -18255,6 +18875,15 @@ "node": ">=8" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-dirs": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", @@ -18691,6 +19320,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -18803,6 +19438,12 @@ "node": ">=0.6" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "dev": true + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -18849,6 +19490,15 @@ "node": ">=18" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", @@ -19048,6 +19698,32 @@ "node": ">= 0.6" } }, + "node_modules/typechat": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/typechat/-/typechat-0.0.10.tgz", + "integrity": "sha512-iF/wLLaZWt4Q9WO8stpq3NKilAa4b8hnCD16EirdhaxzAYk80MCb1wnW1il7GhkMNJuhJUD38dxs8q4A/EdxJw==", + "dev": true, + "dependencies": { + "axios": "^1.4.0", + "typescript": "^5.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typechat/node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -19757,6 +20433,100 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/winston": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", + "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "dev": true, + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.6.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", + "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", + "dev": true, + "dependencies": { + "logform": "^2.6.1", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston-transport/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/winston/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/wipe-node-cache": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-2.1.2.tgz", @@ -20055,18 +20825,6 @@ "node": ">=10" } }, - "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/yargs-unparser/node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -20123,6 +20881,15 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.23.3", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.3.tgz", + "integrity": "sha512-TYWChTxKQbRJp5ST22o/Irt9KC5nj7CdBKYB/AosCRdj/wxEMvv4NNaj9XVUHDOIp53ZxArGhnw5HMZziPFjog==", + "dev": true, + "peerDependencies": { + "zod": "^3.23.3" + } } } } diff --git a/package.json b/package.json index 83e1250ae..61ea98fe2 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "update-grammar": "ts-node ./scripts/update-grammar.ts", "precompile": "npm run clean", "compile": "npm-run-all compile:*", - "compile:keyfile": "ts-node ./scripts/generate-keyfile.ts", + "compile:constants": "ts-node ./scripts/generate-constants.ts", "compile:resources": "npm run update-grammar", "compile:extension": "tsc -p ./", "compile:extension-bundles": "webpack --mode development", @@ -56,7 +56,7 @@ "test-webview": "mocha -r ts-node/register --file ./src/test/setup-webview.ts src/test/suite/views/webview-app/**/*.test.tsx", "ai-accuracy-tests": "mocha -r ts-node/register --file ./src/test/ai-accuracy-tests/test-setup.ts ./src/test/ai-accuracy-tests/ai-accuracy-tests.ts", "analyze-bundle": "webpack --mode production --analyze", - "vscode:prepublish": "npm run clean && npm run compile:keyfile && npm run compile:resources && webpack --mode production", + "vscode:prepublish": "npm run clean && npm run compile:constants && npm run compile:resources && webpack --mode production", "check": "npm run lint && npm run depcheck", "depcheck": "depcheck", "package": "cross-env NODE_OPTIONS='--require ./scripts/no-npm-list-fail.js' vsce package --githubBranch main", @@ -94,6 +94,11 @@ "name": "query", "isSticky": true, "description": "Ask how to write MongoDB queries or pipelines. For example, you can ask: \"Show me all the documents where the address contains the word street\"." + }, + { + "name": "docs", + "isSticky": true, + "description": "Ask MongoDB-related questions and find answers in the official documentation." } ] } @@ -1202,6 +1207,7 @@ "@mongodb-js/oidc-mock-provider": "^0.9.1", "@mongodb-js/oidc-plugin": "^0.4.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", + "mongodb-rag-core": "^0.4.1", "@mongodb-js/sbom-tools": "^0.7.1", "@mongodb-js/signing-utils": "^0.3.5", "@mongosh/service-provider-core": "^2.2.15", diff --git a/scripts/generate-constants.ts b/scripts/generate-constants.ts new file mode 100644 index 000000000..19c738210 --- /dev/null +++ b/scripts/generate-constants.ts @@ -0,0 +1,31 @@ +#! /usr/bin/env ts-node + +import ora from 'ora'; +import fs from 'fs'; +import path from 'path'; +import { resolve } from 'path'; +import { config } from 'dotenv'; +import { promisify } from 'util'; + +const writeFile = promisify(fs.writeFile); +const ROOT_DIR = path.join(__dirname, '..'); +const ui = ora('Generate constants file').start(); + +config({ path: resolve(__dirname, '../.env') }); + +(async () => { + await writeFile( + `${ROOT_DIR}/constants.json`, + JSON.stringify( + { + segmentKey: process.env.SEGMENT_KEY, + docsChatbotBaseUri: process.env.MONGODB_DOCS_CHATBOT_BASE_URI, + }, + null, + 2 + ) + ); + ui.succeed('The constants file has been generated'); +})().catch((error) => { + ui.fail(`Failed to generate constants file: ${error.message}`); +}); diff --git a/scripts/generate-keyfile.ts b/scripts/generate-keyfile.ts deleted file mode 100644 index b7a6add73..000000000 --- a/scripts/generate-keyfile.ts +++ /dev/null @@ -1,28 +0,0 @@ -#! /usr/bin/env ts-node - -import ora from 'ora'; -import fs from 'fs'; -import path from 'path'; -import { resolve } from 'path'; -import { config } from 'dotenv'; -import { promisify } from 'util'; - -const writeFile = promisify(fs.writeFile); -const ROOT_DIR = path.join(__dirname, '..'); -const ui = ora('Generate constants keyfile').start(); - -config({ path: resolve(__dirname, '../.env') }); - -(async () => { - if (process.env.SEGMENT_KEY) { - await writeFile( - `${ROOT_DIR}/constants.json`, - JSON.stringify({ segmentKey: process.env.SEGMENT_KEY }, null, 2) - ); - ui.succeed('Generated segment constants file'); - } else { - throw new Error('The Segment key is missing in environment variables'); - } -})().catch((error) => { - ui.fail(`Failed to generate segment constants file: ${error.message}`); -}); diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index a72aee212..98bbf9e7d 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -155,6 +155,7 @@ export default class MDBExtensionController implements vscode.Disposable { this._telemetryService.activateSegmentAnalytics(); this._participantController.createParticipant(this._context); + await this._participantController.createDocsChatbot(this._context); await this._connectionController.loadSavedConnections(); await this._languageServerController.startLanguageServer(); diff --git a/src/participant/chatMetadata.ts b/src/participant/chatMetadata.ts index 29a196c36..dc31d28ae 100644 --- a/src/participant/chatMetadata.ts +++ b/src/participant/chatMetadata.ts @@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; export type ChatMetadata = { databaseName?: string; collectionName?: string; + docsChatbotConversationId?: string; }; export class ChatMetadataStore { diff --git a/src/participant/constants.ts b/src/participant/constants.ts index 39f560998..b867f0738 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -83,3 +83,12 @@ export function queryRequestChatResult( ): ChatResult { return createChatResult('query', history); } + +export function docsRequestChatResult(chatId: string): ChatResult { + return { + metadata: { + chatId, + intent: 'docs', + }, + }; +} diff --git a/src/participant/docsChatbotAIService.ts b/src/participant/docsChatbotAIService.ts new file mode 100644 index 000000000..d73de13f5 --- /dev/null +++ b/src/participant/docsChatbotAIService.ts @@ -0,0 +1,130 @@ +import type { Reference, VerifiedAnswer } from 'mongodb-rag-core'; + +const MONGODB_DOCS_CHATBOT_API_VERSION = 'v1'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { version } = require('../../package.json'); + +type Role = 'user' | 'assistant'; + +type ConversationData = { + _id: string; + createdAt: string; + messages: MessageData[]; + conversationId: string; +}; + +type MessageData = { + id: string; + role: Role; + content: string; + createdAt: string; + rating?: boolean; + references?: Reference[]; + suggestedPrompts?: string[]; + metadata?: AssistantMessageMetadata; +}; + +type AssistantMessageMetadata = { + [k: string]: unknown; + + /** + If the message came from the verified answers collection, contains the + metadata about the verified answer. + */ + verifiedAnswer?: { + _id: VerifiedAnswer['_id']; + created: string; + updated: string | undefined; + }; +}; + +export class DocsChatbotAIService { + _serverBaseUri?: string; + + constructor(serverBaseUri?: string) { + this._serverBaseUri = serverBaseUri; + } + + private getServerBaseUri(): string { + if (!this._serverBaseUri) { + throw new Error( + 'You must define a serverBaseUri for the DocsChatbotAIService' + ); + } + return this._serverBaseUri; + } + + private getUri(path: string): string { + const serverBaseUri = this.getServerBaseUri(); + return `${serverBaseUri}api/${MONGODB_DOCS_CHATBOT_API_VERSION}${path}`; + } + + async createConversation(): Promise { + const uri = this.getUri('/conversations'); + return this._fetch({ + uri, + method: 'POST', + }); + } + + async _fetch({ + uri, + method, + body, + headers, + }: { + uri: string; + method: string; + body?: string; + headers?: { [key: string]: string }; + }): Promise { + const resp = await fetch(uri, { + headers: { + origin: this.getServerBaseUri(), + 'User-Agent': `mongodb-vscode/${version}`, + ...headers, + }, + method, + ...(body && { body }), + }); + let conversation; + try { + conversation = await resp.json(); + } catch (error) { + throw new Error('[Docs chatbot] Internal server error'); + } + + if (resp.status === 400) { + throw new Error(`[Docs chatbot] Bad request: ${conversation.error}`); + } + if (resp.status === 429) { + throw new Error(`[Docs chatbot] Rate limited: ${conversation.error}`); + } + if (resp.status >= 500) { + throw new Error( + `[Docs chatbot] Internal server error: ${conversation.error}` + ); + } + return { + ...conversation, + conversationId: conversation._id, + }; + } + + async addMessage({ + conversationId, + message, + }: { + conversationId: string; + message: string; + }): Promise { + const uri = this.getUri(`/conversations/${conversationId}/messages`); + return await this._fetch({ + uri, + method: 'POST', + body: JSON.stringify({ message }), + headers: { 'Content-Type': 'application/json' }, + }); + } +} diff --git a/src/participant/markdown.ts b/src/participant/markdown.ts index 75ca9ed4b..df377b4a9 100644 --- a/src/participant/markdown.ts +++ b/src/participant/markdown.ts @@ -23,10 +23,10 @@ export function createMarkdownLink({ ) : undefined; const commandQueryString = data ? `?${encodedData}` : ''; - const connName = new vscode.MarkdownString( + const link = new vscode.MarkdownString( `- ${name}\n` ); - connName.supportHtml = true; - connName.isTrusted = { enabledCommands: [commandId] }; - return connName; + link.supportHtml = true; + link.isTrusted = { enabledCommands: [commandId] }; + return link; } diff --git a/src/participant/participant.ts b/src/participant/participant.ts index bd2f92580..4ccd57eff 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1,6 +1,9 @@ import * as vscode from 'vscode'; import { getSimplifiedSchema } from 'mongodb-schema'; import type { Document } from 'bson'; +import { config } from 'dotenv'; +import path from 'path'; +import { promises as fs } from 'fs'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; @@ -17,6 +20,7 @@ import { genericRequestChatResult, namespaceRequestChatResult, queryRequestChatResult, + docsRequestChatResult, } from './constants'; import { QueryPrompt } from './prompts/query'; import { COL_NAME_ID, DB_NAME_ID, NamespacePrompt } from './prompts/namespace'; @@ -25,16 +29,20 @@ import { getSimplifiedSampleDocuments } from './sampleDocuments'; import { getCopilotModel } from './model'; import { createMarkdownLink } from './markdown'; import { ChatMetadataStore } from './chatMetadata'; +import { DocsChatbotAIService } from './docsChatbotAIService'; import { chatResultFeedbackKindToTelemetryValue, TelemetryEventTypes, } from '../telemetry/telemetryService'; import type TelemetryService from '../telemetry/telemetryService'; +import type { Reference } from 'mongodb-rag-core'; const log = createLogger('participant'); const NUM_DOCUMENTS_TO_SAMPLE = 3; +const MONGODB_DOCS_LINK = 'https://www.mongodb.com/docs/'; + interface NamespaceQuickPicks { label: string; data: string; @@ -73,6 +81,7 @@ export default class ParticipantController { _connectionController: ConnectionController; _storageController: StorageController; _chatMetadataStore: ChatMetadataStore; + _docsChatbotAIService?: DocsChatbotAIService; _telemetryService: TelemetryService; constructor({ @@ -90,6 +99,37 @@ export default class ParticipantController { this._telemetryService = telemetryService; } + // To integrate with the MongoDB documentation chatbot, + // set the MONGODB_DOCS_CHATBOT_BASE_URI environment variable when running the extension from a branch. + // This variable is automatically injected during the .vsix build process via GitHub Actions. + async _readDocsChatbotBaseUri( + context: vscode.ExtensionContext + ): Promise { + config({ path: path.join(context.extensionPath, '.env') }); + + try { + const docsChatbotBaseUriFileLocation = path.join( + context.extensionPath, + './constants.json' + ); + // eslint-disable-next-line no-sync + const constantsFile = await fs.readFile( + docsChatbotBaseUriFileLocation, + 'utf8' + ); + const { docsChatbotBaseUri } = JSON.parse(constantsFile) as { + docsChatbotBaseUri?: string; + }; + return docsChatbotBaseUri; + } catch (error) { + log.error( + 'Failed to read docsChatbotBaseUri from the constants file', + error + ); + return; + } + } + createParticipant(context: vscode.ExtensionContext): vscode.ChatParticipant { // Chat participants appear as top-level options in the chat input // when you type `@`, and can contribute sub-commands in the chat input @@ -110,6 +150,11 @@ export default class ParticipantController { return this._participant; } + async createDocsChatbot(context: vscode.ExtensionContext): Promise { + const docsChatbotBaseUri = await this._readDocsChatbotBaseUri(context); + this._docsChatbotAIService = new DocsChatbotAIService(docsChatbotBaseUri); + } + getParticipant(): vscode.ChatParticipant | undefined { return this._participant; } @@ -175,6 +220,31 @@ export default class ParticipantController { return responseContent; } + _streamRunnableContentActions({ + responseContent, + stream, + }: { + responseContent: string; + stream: vscode.ChatResponseStream; + }): void { + const runnableContent = getRunnableContentFromString(responseContent); + if (runnableContent) { + const commandArgs: RunParticipantQueryCommandArgs = { + runnableContent, + }; + stream.button({ + command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, + title: vscode.l10n.t('▶️ Run'), + arguments: [commandArgs], + }); + stream.button({ + command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + title: vscode.l10n.t('Open in playground'), + arguments: [commandArgs], + }); + } + } + // @MongoDB what is mongodb? async handleGenericRequest( request: vscode.ChatRequest, @@ -198,22 +268,10 @@ export default class ParticipantController { }); stream.markdown(responseContent); - const runnableContent = getRunnableContentFromString(responseContent); - if (runnableContent) { - const commandArgs: RunParticipantQueryCommandArgs = { - runnableContent, - }; - stream.button({ - command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, - title: vscode.l10n.t('▶️ Run'), - arguments: [commandArgs], - }); - stream.button({ - command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, - title: vscode.l10n.t('Open in playground'), - arguments: [commandArgs], - }); - } + this._streamRunnableContentActions({ + responseContent, + stream, + }); return genericRequestChatResult(context.history); } @@ -730,24 +788,148 @@ export default class ParticipantController { stream.markdown(responseContent); - const runnableContent = getRunnableContentFromString(responseContent); - if (runnableContent) { - const commandArgs: RunParticipantQueryCommandArgs = { - runnableContent, - }; - stream.button({ - command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, - title: vscode.l10n.t('▶️ Run'), - arguments: [commandArgs], + this._streamRunnableContentActions({ + responseContent, + stream, + }); + + return queryRequestChatResult(context.history); + } + + async _handleDocsRequestWithChatbot({ + docsChatbotAIService, + prompt, + chatId, + }: { + docsChatbotAIService: DocsChatbotAIService; + prompt: string; + chatId: string; + }): Promise<{ + responseContent: string; + responseReferences?: Reference[]; + }> { + let { docsChatbotConversationId } = + this._chatMetadataStore.getChatMetadata(chatId) ?? {}; + if (!docsChatbotConversationId) { + const conversation = await docsChatbotAIService.createConversation(); + docsChatbotConversationId = conversation._id; + this._chatMetadataStore.setChatMetadata(chatId, { + docsChatbotConversationId, }); - stream.button({ - command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, - title: vscode.l10n.t('Open in playground'), - arguments: [commandArgs], + } + + const response = await docsChatbotAIService.addMessage({ + message: prompt, + conversationId: docsChatbotConversationId, + }); + + return { + responseContent: response.content, + responseReferences: response.references, + }; + } + + async _handleDocsRequestWithCopilot( + ...args: [ + vscode.ChatRequest, + vscode.ChatContext, + vscode.ChatResponseStream, + vscode.CancellationToken + ] + ): Promise<{ + responseContent: string; + responseReferences?: Reference[]; + }> { + const [request, context, stream, token] = args; + const messages = GenericPrompt.buildMessages({ + request, + context, + }); + + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); + const responseContent = await this.getChatResponseContent({ + messages, + stream, + token, + }); + const responseReferences = [ + { + url: MONGODB_DOCS_LINK, + title: 'View MongoDB documentation', + }, + ]; + + return { + responseContent, + responseReferences, + }; + } + + async handleDocsRequest( + ...args: [ + vscode.ChatRequest, + vscode.ChatContext, + vscode.ChatResponseStream, + vscode.CancellationToken + ] + ): Promise { + const [request, context, stream, token] = args; + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); + + const chatId = ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ); + + let docsChatbotHasThrownError = false; + let docsResult: { + responseContent?: string; + responseReferences?: Reference[]; + } = {}; + + if (this._docsChatbotAIService) { + try { + docsResult = await this._handleDocsRequestWithChatbot({ + docsChatbotAIService: this._docsChatbotAIService, + prompt: request.prompt, + chatId, + }); + } catch (error) { + // If the docs chatbot API is not available, fall back to Copilot’s LLM and include + // the MongoDB documentation link for users to go to our documentation site directly. + docsChatbotHasThrownError = true; + log.error(error); + } + } + + if (!this._docsChatbotAIService || docsChatbotHasThrownError) { + docsResult = await this._handleDocsRequestWithCopilot(...args); + } + + if (docsResult.responseContent) { + stream.markdown(docsResult.responseContent); + this._streamRunnableContentActions({ + responseContent: docsResult.responseContent, + stream, }); } - return queryRequestChatResult(context.history); + if (docsResult.responseReferences) { + for (const ref of docsResult.responseReferences) { + const link = new vscode.MarkdownString( + `- ${ref.title}\n` + ); + link.supportHtml = true; + stream.markdown(link); + } + } + + return docsRequestChatResult(chatId); } async chatHandler( @@ -792,7 +974,7 @@ export default class ParticipantController { if (request.command === 'query') { return await this.handleQueryRequest(...args); } else if (request.command === 'docs') { - // TODO(VSCODE-570): Implement this. + return await this.handleDocsRequest(...args); } else if (request.command === 'schema') { // TODO(VSCODE-571): Implement this. } diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index f7ff48731..41fe33b4c 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -187,13 +187,12 @@ export default class TelemetryService { ); // eslint-disable-next-line no-sync const constantsFile = fs.readFileSync(segmentKeyFileLocation, 'utf8'); - const constants = JSON.parse(constantsFile) as { segmentKey: string }; - - log.info('SegmentKey was found', { type: typeof constants.segmentKey }); - - return constants.segmentKey; + const { segmentKey } = JSON.parse(constantsFile) as { + segmentKey?: string; + }; + return segmentKey; } catch (error) { - log.error('SegmentKey was not found', error); + log.error('Failed to read segmentKey from the constants file', error); return; } } @@ -275,7 +274,7 @@ export default class TelemetryService { async _getConnectionTelemetryProperties( dataService: DataService, connectionType: ConnectionTypes - ) { + ): Promise { return await getConnectionTelemetryProperties(dataService, connectionType); } @@ -323,7 +322,9 @@ export default class TelemetryService { return 'other'; } - getTelemetryUserIdentity() { + getTelemetryUserIdentity(): { + anonymousId: string; + } { return { anonymousId: this._segmentAnonymousId, }; @@ -388,7 +389,7 @@ export default class TelemetryService { trackSavedConnectionsLoaded( savedConnectionsLoadedProps: SavedConnectionsLoadedProperties - ) { + ): void { this.track( TelemetryEventTypes.SAVED_CONNECTIONS_LOADED, savedConnectionsLoadedProps @@ -397,7 +398,7 @@ export default class TelemetryService { trackKeytarSecretsMigrationFailed( keytarSecretsMigrationFailedProps: KeytarSecretsMigrationFailedProperties - ) { + ): void { this.track( TelemetryEventTypes.KEYTAR_SECRETS_MIGRATION_FAILED, keytarSecretsMigrationFailedProps diff --git a/src/test/suite/participant/docsChatbotAIService.test.ts b/src/test/suite/participant/docsChatbotAIService.test.ts new file mode 100644 index 000000000..6860c7a0b --- /dev/null +++ b/src/test/suite/participant/docsChatbotAIService.test.ts @@ -0,0 +1,108 @@ +import { beforeEach, afterEach } from 'mocha'; +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { DocsChatbotAIService } from '../../../participant/docsChatbotAIService'; + +suite('DocsChatbotAIService Test Suite', function () { + const initialFetch = global.fetch; + + afterEach(function () { + global.fetch = initialFetch; + sinon.restore(); + }); + + suite('when serverBaseUri is missing', function () { + test('DocsChatbotAIService constructor does not throw', () => { + const docsChatbotAIService = new DocsChatbotAIService(); + expect(docsChatbotAIService._serverBaseUri).to.be.undefined; + }); + + test('createConversation throws if serverBaseUri is not set', async () => { + const docsChatbotAIService = new DocsChatbotAIService(); + try { + await docsChatbotAIService.createConversation(); + expect.fail('It must fail with missing serverBaseUri'); + } catch (error) { + expect((error as Error).message).to.include( + 'You must define a serverBaseUri for the DocsChatbotAIService' + ); + } + }); + }); + + suite('when serverBaseUri is present', function () { + const serverBaseUri = 'https://example.com/'; + let docsChatbotAIService: DocsChatbotAIService; + + beforeEach(() => { + docsChatbotAIService = new DocsChatbotAIService(serverBaseUri); + }); + + test('creates conversations', async () => { + const fetchStub = sinon.stub().resolves({ + status: 200, + ok: true, + json: () => + Promise.resolve({ + _id: '650b4b260f975ef031016c8a', + messages: [], + }), + }); + global.fetch = fetchStub; + const conversation = await docsChatbotAIService.createConversation(); + expect(conversation._id).to.be.eql('650b4b260f975ef031016c8a'); + }); + + test('throws on server errors', async () => { + const fetchStub = sinon.stub().resolves({ + status: 500, + ok: false, + statusText: 'Server error', + json: sinon.stub().rejects(new Error('invalid json')), + }); + global.fetch = fetchStub; + + try { + await docsChatbotAIService.createConversation(); + expect.fail('It must fail with the server error'); + } catch (error) { + expect((error as Error).message).to.include('Internal server error'); + } + }); + + test('throws on bad requests', async () => { + const fetchStub = sinon.stub().resolves({ + status: 400, + ok: false, + statusText: 'Client error', + json: sinon.stub().resolves({}), + }); + global.fetch = fetchStub; + + try { + await docsChatbotAIService.createConversation(); + expect.fail('It must fail with the bad request error'); + } catch (error) { + expect((error as Error).message).to.include('Bad request'); + } + }); + + test('throws on a rate limit', async () => { + const fetchStub = sinon.stub().resolves({ + status: 429, + ok: false, + statusText: 'Model error', + json: sinon.stub().resolves({}), + }); + global.fetch = fetchStub; + + try { + await docsChatbotAIService.createConversation(); + expect.fail('It must fail with the rate limited error'); + } catch (error) { + expect((error as Error).message).to.include('Rate limited'); + } + }); + }); +}); diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 7ea3849c3..354a95a95 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1055,6 +1055,62 @@ suite('Participant Controller Test Suite', function () { }); }); }); + + suite('docs command', function () { + const initialFetch = global.fetch; + let fetchStub; + + afterEach(function () { + global.fetch = initialFetch; + sinon.restore(); + }); + + beforeEach(function () { + fetchStub = sinon.stub().resolves({ + status: 200, + ok: true, + json: () => + Promise.resolve({ + _id: '650b4b260f975ef031016c8a', + messages: [], + }), + }); + global.fetch = fetchStub; + sendRequestStub.onCall(0).resolves({ + text: ['connection info'], + }); + }); + + test('uses docs chatbot when it is available', async function () { + sinon.replace( + testParticipantController, + '_readDocsChatbotBaseUri', + sinon.stub().resolves('url') + ); + await testParticipantController.createDocsChatbot( + extensionContextStub + ); + const chatRequestMock = { + prompt: 'how to connect to mongodb', + command: 'docs', + references: [], + }; + await invokeChatHandler(chatRequestMock); + expect(fetchStub).to.have.been.called; + expect(sendRequestStub).to.have.not.been.called; + }); + + test('falls back to the copilot model when docs chatbot api is not available', async function () { + const chatRequestMock = { + prompt: 'how to connect to mongodb', + command: 'docs', + references: [], + }; + await invokeChatHandler(chatRequestMock); + expect(fetchStub).to.have.not.been.called; + expect(sendRequestStub).to.have.been.called; + }); + }); }); }); From 48f4a674446d2c9832308e1cc73a94e291356369 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Sat, 21 Sep 2024 14:42:02 +0200 Subject: [PATCH 25/45] feat(participant): Report errors to telemetry (#823) --- src/participant/participant.ts | 135 ++++++++++-------- src/telemetry/telemetryService.ts | 21 ++- .../suite/participant/participant.test.ts | 55 +++++++ 3 files changed, 144 insertions(+), 67 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 4ccd57eff..75cebb8c8 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -32,6 +32,7 @@ import { ChatMetadataStore } from './chatMetadata'; import { DocsChatbotAIService } from './docsChatbotAIService'; import { chatResultFeedbackKindToTelemetryValue, + ParticipantErrorTypes, TelemetryEventTypes, } from '../telemetry/telemetryService'; import type TelemetryService from '../telemetry/telemetryService'; @@ -159,27 +160,45 @@ export default class ParticipantController { return this._participant; } - handleError(err: any, stream: vscode.ChatResponseStream): void { + handleError(err: any, command: string): never { + let errorCode: string | undefined; + let errorName: ParticipantErrorTypes; // Making the chat request might fail because // - model does not exist // - user consent not given // - quote limits exceeded if (err instanceof vscode.LanguageModelError) { - log.error(err.message, err.code, err.cause); - if ( - err.cause instanceof Error && - err.cause.message.includes('off_topic') - ) { - stream.markdown( - vscode.l10n.t( - "I'm sorry, I can only explain computer science concepts.\n\n" - ) - ); - } + errorCode = err.code; + } + + if (err instanceof Error) { + // Unwrap the error if a cause is provided + err = err.cause || err; + } + + const message: string = err.message || err.toString(); + + if (message.includes('off_topic')) { + errorName = ParticipantErrorTypes.CHAT_MODEL_OFF_TOPIC; + } else if (message.includes('Filtered by Responsible AI Service')) { + errorName = ParticipantErrorTypes.FILTERED; + } else if (message.includes('Prompt failed validation')) { + errorName = ParticipantErrorTypes.INVALID_PROMPT; } else { - // Re-throw other errors so they show up in the UI. - throw err; + errorName = ParticipantErrorTypes.OTHER; } + + this._telemetryService.track( + TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED, + { + command, + error_code: errorCode, + error_name: errorName, + } + ); + + // Re-throw other errors so they show up in the UI. + throw err; } /** @@ -197,23 +216,17 @@ export default class ParticipantController { async getChatResponseContent({ messages, - stream, token, }: { messages: vscode.LanguageModelChatMessage[]; - stream: vscode.ChatResponseStream; token: vscode.CancellationToken; }): Promise { const model = await getCopilotModel(); let responseContent = ''; if (model) { - try { - const chatResponse = await model.sendRequest(messages, {}, token); - for await (const fragment of chatResponse.text) { - responseContent += fragment; - } - } catch (err) { - this.handleError(err, stream); + const chatResponse = await model.sendRequest(messages, {}, token); + for await (const fragment of chatResponse.text) { + responseContent += fragment; } } @@ -263,7 +276,6 @@ export default class ParticipantController { }); const responseContent = await this.getChatResponseContent({ messages, - stream, token, }); stream.markdown(responseContent); @@ -526,12 +538,10 @@ export default class ParticipantController { async _getNamespaceFromChat({ request, context, - stream, token, }: { request: vscode.ChatRequest; context: vscode.ChatContext; - stream: vscode.ChatResponseStream; token: vscode.CancellationToken; }): Promise<{ databaseName: string | undefined; @@ -546,7 +556,6 @@ export default class ParticipantController { }); const responseContentWithNamespace = await this.getChatResponseContent({ messages: messagesWithNamespace, - stream, token, }); const namespace = parseForDatabaseAndCollectionName( @@ -745,7 +754,6 @@ export default class ParticipantController { const { databaseName, collectionName } = await this._getNamespaceFromChat({ request, context, - stream, token, }); if (!databaseName || !collectionName) { @@ -782,7 +790,6 @@ export default class ParticipantController { }); const responseContent = await this.getChatResponseContent({ messages, - stream, token, }); @@ -840,7 +847,7 @@ export default class ParticipantController { responseContent: string; responseReferences?: Reference[]; }> { - const [request, context, stream, token] = args; + const [request, context, , token] = args; const messages = GenericPrompt.buildMessages({ request, context, @@ -852,7 +859,6 @@ export default class ParticipantController { }); const responseContent = await this.getChatResponseContent({ messages, - stream, token, }); const responseReferences = [ @@ -939,46 +945,49 @@ export default class ParticipantController { vscode.ChatResponseStream, vscode.CancellationToken ] - ): Promise { + ): Promise { const [request, , stream] = args; - - if ( - !request.command && - (!request.prompt || request.prompt.trim().length === 0) - ) { - stream.markdown(GenericPrompt.getEmptyRequestResponse()); - return emptyRequestChatResult(args[1].history); - } - - const hasBeenShownWelcomeMessageAlready = !!this._storageController.get( - StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE - ); - if (!hasBeenShownWelcomeMessageAlready) { - stream.markdown( - vscode.l10n.t(` + try { + const hasBeenShownWelcomeMessageAlready = !!this._storageController.get( + StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE + ); + if (!hasBeenShownWelcomeMessageAlready) { + stream.markdown( + vscode.l10n.t(` Welcome to MongoDB Participant!\n\n Interact with your MongoDB clusters and generate MongoDB-related code more efficiently with intelligent AI-powered feature, available today in the MongoDB extension.\n\n Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more information.\n\n`) - ); + ); - this._telemetryService.track( - TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN - ); + this._telemetryService.track( + TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN + ); - await this._storageController.update( - StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE, - true - ); - } + await this._storageController.update( + StorageVariables.COPILOT_HAS_BEEN_SHOWN_WELCOME_MESSAGE, + true + ); + } - if (request.command === 'query') { - return await this.handleQueryRequest(...args); - } else if (request.command === 'docs') { - return await this.handleDocsRequest(...args); - } else if (request.command === 'schema') { - // TODO(VSCODE-571): Implement this. + switch (request.command) { + case 'query': + return await this.handleQueryRequest(...args); + case 'docs': + return await this.handleDocsRequest(...args); + case 'schema': + // TODO(VSCODE-571): Implement this. + return await this.handleGenericRequest(...args); + default: + if (!request.prompt?.trim()) { + stream.markdown(GenericPrompt.getEmptyRequestResponse()); + return emptyRequestChatResult(args[1].history); + } + + return await this.handleGenericRequest(...args); + } + } catch (e) { + this.handleError(e, request.command || 'generic'); } - return await this.handleGenericRequest(...args); } handleUserFeedback(feedback: vscode.ChatResultFeedback): void { diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 41fe33b4c..a9608c8d7 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -102,6 +102,12 @@ type ParticipantFeedbackProperties = { reason?: String; }; +type ParticipantResponseFailedProperties = { + command: string; + error_code?: string; + error_name: string; +}; + export function chatResultFeedbackKindToTelemetryValue( kind: vscode.ChatResultFeedbackKind ): TelemetryFeedbackKind { @@ -130,7 +136,8 @@ type TelemetryEventProperties = | KeytarSecretsMigrationFailedProperties | SavedConnectionsLoadedProperties | SurveyActionProperties - | ParticipantFeedbackProperties; + | ParticipantFeedbackProperties + | ParticipantResponseFailedProperties; export enum TelemetryEventTypes { PLAYGROUND_CODE_EXECUTED = 'Playground Code Executed', @@ -152,6 +159,14 @@ export enum TelemetryEventTypes { SURVEY_DISMISSED = 'Survey prompt dismissed', PARTICIPANT_FEEDBACK = 'Participant Feedback', PARTICIPANT_WELCOME_SHOWN = 'Participant Welcome Shown', + PARTICIPANT_RESPONSE_FAILED = 'Participant Response Failed', +} + +export enum ParticipantErrorTypes { + CHAT_MODEL_OFF_TOPIC = 'Chat Model Off Topic', + INVALID_PROMPT = 'Invalid Prompt', + FILTERED = 'Filtered by Responsible AI Service', + OTHER = 'Other', } /** @@ -322,9 +337,7 @@ export default class TelemetryService { return 'other'; } - getTelemetryUserIdentity(): { - anonymousId: string; - } { + getTelemetryUserIdentity(): { anonymousId: string } { return { anonymousId: this._segmentAnonymousId, }; diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 354a95a95..be46143d2 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1169,5 +1169,60 @@ suite('Participant Controller Test Suite', function () { .to.not.include('SSN') .and.not.include('123456789'); }); + + test('reports error', function () { + const err = Error('Filtered by Responsible AI Service'); + expect(() => testParticipantController.handleError(err, 'query')).throws( + 'Filtered by Responsible AI Service' + ); + sinon.assert.calledOnce(telemetryTrackStub); + + expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( + 'Participant Response Failed' + ); + + const properties = telemetryTrackStub.lastCall.args[1]; + expect(properties.command).to.be.equal('query'); + expect(properties.error_code).to.be.undefined; + expect(properties.error_name).to.be.equal( + 'Filtered by Responsible AI Service' + ); + }); + + test('reports nested error', function () { + const err = new Error('Parent error'); + err.cause = Error('This message is flagged as off topic: off_topic.'); + expect(() => testParticipantController.handleError(err, 'docs')).throws( + 'off_topic' + ); + sinon.assert.calledOnce(telemetryTrackStub); + + expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( + 'Participant Response Failed' + ); + + const properties = telemetryTrackStub.lastCall.args[1]; + expect(properties.command).to.be.equal('docs'); + expect(properties.error_code).to.be.undefined; + expect(properties.error_name).to.be.equal('Chat Model Off Topic'); + }); + + test('Reports error code when available', function () { + // eslint-disable-next-line new-cap + const err = vscode.LanguageModelError.NotFound('Model not found'); + expect(() => testParticipantController.handleError(err, 'schema')).throws( + 'Model not found' + ); + sinon.assert.calledOnce(telemetryTrackStub); + + expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( + 'Participant Response Failed' + ); + + const properties = telemetryTrackStub.lastCall.args[1]; + expect(properties.command).to.be.equal('schema'); + expect(properties.error_code).to.be.equal('NotFound'); + expect(properties.error_name).to.be.equal('Other'); + }); }); }); From 077f5389dae804baf9dd39638df0c3d7101f4dbc Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Sat, 21 Sep 2024 14:43:33 +0200 Subject: [PATCH 26/45] fix: incorrect intent checking for namespace messages (#822) --- .vscode/launch.json | 2 +- src/participant/prompts/history.ts | 4 ++-- src/participant/prompts/namespace.ts | 31 +++++++++++++++------------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2ca2ae136..e7f6ca212 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "--extensionDevelopmentPath=${workspaceFolder}" ], "outFiles": [ - "${workspaceFolder}/out/**/*.js" + "${workspaceFolder}/dist/**/*.js" ], "preLaunchTask": "${defaultBuildTask}" }, diff --git a/src/participant/prompts/history.ts b/src/participant/prompts/history.ts index d14ded4bd..58223876e 100644 --- a/src/participant/prompts/history.ts +++ b/src/participant/prompts/history.ts @@ -39,7 +39,7 @@ export function getHistoryMessages({ ]; if ( responseTypesToSkip.indexOf( - (historyItem.result as ChatResult)?.metadata.intent + (historyItem.result as ChatResult)?.metadata?.intent ) > -1 ) { continue; @@ -50,7 +50,7 @@ export function getHistoryMessages({ message += fragment.value.value; if ( - (historyItem.result as ChatResult)?.metadata.intent === + (historyItem.result as ChatResult)?.metadata?.intent === 'askForNamespace' ) { // When the message is the assistant asking for part of a namespace, diff --git a/src/participant/prompts/namespace.ts b/src/participant/prompts/namespace.ts index f311d5c2b..787517aa2 100644 --- a/src/participant/prompts/namespace.ts +++ b/src/participant/prompts/namespace.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { getHistoryMessages } from './history'; +import type { ChatResult } from '../constants'; export const DB_NAME_ID = 'DATABASE_NAME'; export const COL_NAME_ID = 'COLLECTION_NAME'; @@ -64,20 +65,22 @@ No names found. // message was to connect. We want to use the last // message they sent before the connection name as their prompt. let userPrompt = request.prompt; - if ( - connectionNames.includes(request.prompt) && - (context.history[context.history.length - 1] as vscode.ChatResponseTurn) - ?.result?.metadata?.askToConnect - ) { - // Go through the history in reverse order to find the last user message. - for (let i = historyMessages.length - 1; i >= 0; i--) { - if ( - historyMessages[i].role === vscode.LanguageModelChatMessageRole.User - ) { - userPrompt = historyMessages[i].content; - // Remove the item from the history messages array. - historyMessages = historyMessages.slice(0, i); - break; + if (connectionNames.includes(request.prompt)) { + const previousResponse = context.history[ + context.history.length - 1 + ] as vscode.ChatResponseTurn; + const intent = (previousResponse?.result as ChatResult).metadata.intent; + if (intent === 'askToConnect') { + // Go through the history in reverse order to find the last user message. + for (let i = historyMessages.length - 1; i >= 0; i--) { + if ( + historyMessages[i].role === vscode.LanguageModelChatMessageRole.User + ) { + userPrompt = historyMessages[i].content; + // Remove the item from the history messages array. + historyMessages = historyMessages.slice(0, i); + break; + } } } } From b26c19ffcbf1b84b9c55df86bbd6f0a67d106000 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Sun, 22 Sep 2024 15:22:16 +0200 Subject: [PATCH 27/45] feat(participant): rate docs chatbot answers VSCODE-612 (#824) --- .github/workflows/draft-release.yaml | 1 - .github/workflows/test-and-build.yaml | 1 - scripts/generate-constants.ts | 9 +- src/mdbExtensionController.ts | 1 - src/participant/constants.ts | 10 +- src/participant/docsChatbotAIService.ts | 140 +++++++++++---- src/participant/participant.ts | 127 +++++++------- .../participant/docsChatbotAIService.test.ts | 163 ++++++++++-------- .../suite/participant/participant.test.ts | 40 ++--- 9 files changed, 281 insertions(+), 211 deletions(-) diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index 62ec8fb49..2246bc3ac 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -80,7 +80,6 @@ jobs: uses: ./.github/workflows/actions/test-and-build with: SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_PROD }} - MONGODB_DOCS_CHATBOT_BASE_URI: ${{ secrets.MONGODB_DOCS_CHATBOT_BASE_URI_PROD }} ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index 73fc6ef88..ceaaa04f3 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -36,7 +36,6 @@ jobs: uses: ./.github/workflows/actions/test-and-build with: SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_PROD }} - MONGODB_DOCS_CHATBOT_BASE_URI: ${{ secrets.MONGODB_DOCS_CHATBOT_BASE_URI_PROD }} ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} diff --git a/scripts/generate-constants.ts b/scripts/generate-constants.ts index 19c738210..e84d60000 100644 --- a/scripts/generate-constants.ts +++ b/scripts/generate-constants.ts @@ -16,14 +16,7 @@ config({ path: resolve(__dirname, '../.env') }); (async () => { await writeFile( `${ROOT_DIR}/constants.json`, - JSON.stringify( - { - segmentKey: process.env.SEGMENT_KEY, - docsChatbotBaseUri: process.env.MONGODB_DOCS_CHATBOT_BASE_URI, - }, - null, - 2 - ) + JSON.stringify({ segmentKey: process.env.SEGMENT_KEY }, null, 2) ); ui.succeed('The constants file has been generated'); })().catch((error) => { diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 98bbf9e7d..a72aee212 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -155,7 +155,6 @@ export default class MDBExtensionController implements vscode.Disposable { this._telemetryService.activateSegmentAnalytics(); this._participantController.createParticipant(this._context); - await this._participantController.createDocsChatbot(this._context); await this._connectionController.loadSavedConnections(); await this._languageServerController.startLanguageServer(); diff --git a/src/participant/constants.ts b/src/participant/constants.ts index b867f0738..59f80d4f4 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -16,6 +16,7 @@ export type ParticipantResponseType = interface Metadata { intent: Exclude; chatId: string; + docsChatbotMessageId?: string; } interface AskForNamespaceMetadata { @@ -84,11 +85,18 @@ export function queryRequestChatResult( return createChatResult('query', history); } -export function docsRequestChatResult(chatId: string): ChatResult { +export function docsRequestChatResult({ + chatId, + docsChatbotMessageId, +}: { + chatId: string; + docsChatbotMessageId?: string; +}): ChatResult { return { metadata: { chatId, intent: 'docs', + docsChatbotMessageId, }, }; } diff --git a/src/participant/docsChatbotAIService.ts b/src/participant/docsChatbotAIService.ts index d73de13f5..4b0c8b1b8 100644 --- a/src/participant/docsChatbotAIService.ts +++ b/src/participant/docsChatbotAIService.ts @@ -1,5 +1,7 @@ import type { Reference, VerifiedAnswer } from 'mongodb-rag-core'; +const MONGODB_DOCS_CHATBOT_BASE_URI = 'https://knowledge.mongodb.com/'; + const MONGODB_DOCS_CHATBOT_API_VERSION = 'v1'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -40,35 +42,19 @@ type AssistantMessageMetadata = { }; export class DocsChatbotAIService { - _serverBaseUri?: string; + _serverBaseUri: string; - constructor(serverBaseUri?: string) { - this._serverBaseUri = serverBaseUri; - } - - private getServerBaseUri(): string { - if (!this._serverBaseUri) { - throw new Error( - 'You must define a serverBaseUri for the DocsChatbotAIService' - ); - } - return this._serverBaseUri; + constructor() { + this._serverBaseUri = + process.env.MONGODB_DOCS_CHATBOT_BASE_URI_OVERRIDE || + MONGODB_DOCS_CHATBOT_BASE_URI; } private getUri(path: string): string { - const serverBaseUri = this.getServerBaseUri(); - return `${serverBaseUri}api/${MONGODB_DOCS_CHATBOT_API_VERSION}${path}`; - } - - async createConversation(): Promise { - const uri = this.getUri('/conversations'); - return this._fetch({ - uri, - method: 'POST', - }); + return `${this._serverBaseUri}api/${MONGODB_DOCS_CHATBOT_API_VERSION}${path}`; } - async _fetch({ + async _fetch({ uri, method, body, @@ -78,37 +64,49 @@ export class DocsChatbotAIService { method: string; body?: string; headers?: { [key: string]: string }; - }): Promise { - const resp = await fetch(uri, { + }): Promise { + return fetch(uri, { headers: { - origin: this.getServerBaseUri(), + origin: this._serverBaseUri, 'User-Agent': `mongodb-vscode/${version}`, ...headers, }, method, ...(body && { body }), }); - let conversation; + } + + async createConversation(): Promise { + const uri = this.getUri('/conversations'); + const res = await this._fetch({ + uri, + method: 'POST', + }); + + let data; try { - conversation = await resp.json(); + data = await res.json(); } catch (error) { throw new Error('[Docs chatbot] Internal server error'); } - if (resp.status === 400) { - throw new Error(`[Docs chatbot] Bad request: ${conversation.error}`); + if (res.status === 400) { + throw new Error(`[Docs chatbot] Bad request: ${data.error}`); } - if (resp.status === 429) { - throw new Error(`[Docs chatbot] Rate limited: ${conversation.error}`); + if (res.status === 429) { + throw new Error(`[Docs chatbot] Rate limited: ${data.error}`); } - if (resp.status >= 500) { + if (res.status >= 500) { throw new Error( - `[Docs chatbot] Internal server error: ${conversation.error}` + `[Docs chatbot] Internal server error: ${ + data.error ? data.error : `${res.status} - ${res.statusText}}` + }` ); } + return { - ...conversation, - conversationId: conversation._id, + ...data, + conversationId: data._id, }; } @@ -120,11 +118,79 @@ export class DocsChatbotAIService { message: string; }): Promise { const uri = this.getUri(`/conversations/${conversationId}/messages`); - return await this._fetch({ + const res = await this._fetch({ uri, method: 'POST', body: JSON.stringify({ message }), headers: { 'Content-Type': 'application/json' }, }); + + let data; + try { + data = await res.json(); + } catch (error) { + throw new Error('[Docs chatbot] Internal server error'); + } + + if (res.status === 400) { + throw new Error(`[Docs chatbot] Bad request: ${data.error}`); + } + if (res.status === 404) { + throw new Error(`[Docs chatbot] Conversation not found: ${data.error}`); + } + if (res.status === 429) { + throw new Error(`[Docs chatbot] Rate limited: ${data.error}`); + } + if (res.status === 504) { + throw new Error(`[Docs chatbot] Timeout: ${data.error}`); + } + if (res.status >= 500) { + throw new Error( + `[Docs chatbot] Internal server error: ${ + data.error ? data.error : `${res.status} - ${res.statusText}}` + }` + ); + } + + return data; + } + + async rateMessage({ + conversationId, + messageId, + rating, + }: { + conversationId: string; + messageId: string; + rating: boolean; + }): Promise { + const uri = this.getUri( + `/conversations/${conversationId}/messages/${messageId}/rating` + ); + const res = await this._fetch({ + uri, + method: 'POST', + body: JSON.stringify({ rating }), + headers: { 'Content-Type': 'application/json' }, + }); + + if (res.status === 204) { + return rating; + } + + let data; + if (res.status >= 400) { + try { + data = await res.json(); + } catch (error) { + throw new Error(`[Docs chatbot] Internal server error: ${error}`); + } + } + + throw new Error( + `[Docs chatbot] Internal server error: ${ + data.error ? data.error : `${res.status} - ${res.statusText}}` + }` + ); } } diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 75cebb8c8..5f2eb274d 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1,9 +1,6 @@ import * as vscode from 'vscode'; import { getSimplifiedSchema } from 'mongodb-schema'; import type { Document } from 'bson'; -import { config } from 'dotenv'; -import path from 'path'; -import { promises as fs } from 'fs'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; @@ -82,7 +79,7 @@ export default class ParticipantController { _connectionController: ConnectionController; _storageController: StorageController; _chatMetadataStore: ChatMetadataStore; - _docsChatbotAIService?: DocsChatbotAIService; + _docsChatbotAIService: DocsChatbotAIService; _telemetryService: TelemetryService; constructor({ @@ -98,37 +95,7 @@ export default class ParticipantController { this._storageController = storageController; this._chatMetadataStore = new ChatMetadataStore(); this._telemetryService = telemetryService; - } - - // To integrate with the MongoDB documentation chatbot, - // set the MONGODB_DOCS_CHATBOT_BASE_URI environment variable when running the extension from a branch. - // This variable is automatically injected during the .vsix build process via GitHub Actions. - async _readDocsChatbotBaseUri( - context: vscode.ExtensionContext - ): Promise { - config({ path: path.join(context.extensionPath, '.env') }); - - try { - const docsChatbotBaseUriFileLocation = path.join( - context.extensionPath, - './constants.json' - ); - // eslint-disable-next-line no-sync - const constantsFile = await fs.readFile( - docsChatbotBaseUriFileLocation, - 'utf8' - ); - const { docsChatbotBaseUri } = JSON.parse(constantsFile) as { - docsChatbotBaseUri?: string; - }; - return docsChatbotBaseUri; - } catch (error) { - log.error( - 'Failed to read docsChatbotBaseUri from the constants file', - error - ); - return; - } + this._docsChatbotAIService = new DocsChatbotAIService(); } createParticipant(context: vscode.ExtensionContext): vscode.ChatParticipant { @@ -144,18 +111,13 @@ export default class ParticipantController { 'images', 'mongodb.png' ); - log.info('Chat Participant Created', { + log.info('Chat participant created', { participantId: this._participant?.id, }); this._participant.onDidReceiveFeedback(this.handleUserFeedback.bind(this)); return this._participant; } - async createDocsChatbot(context: vscode.ExtensionContext): Promise { - const docsChatbotBaseUri = await this._readDocsChatbotBaseUri(context); - this._docsChatbotAIService = new DocsChatbotAIService(docsChatbotBaseUri); - } - getParticipant(): vscode.ChatParticipant | undefined { return this._participant; } @@ -804,35 +766,43 @@ export default class ParticipantController { } async _handleDocsRequestWithChatbot({ - docsChatbotAIService, prompt, chatId, }: { - docsChatbotAIService: DocsChatbotAIService; prompt: string; chatId: string; }): Promise<{ responseContent: string; responseReferences?: Reference[]; + docsChatbotMessageId: string; }> { let { docsChatbotConversationId } = this._chatMetadataStore.getChatMetadata(chatId) ?? {}; if (!docsChatbotConversationId) { - const conversation = await docsChatbotAIService.createConversation(); + const conversation = + await this._docsChatbotAIService.createConversation(); docsChatbotConversationId = conversation._id; this._chatMetadataStore.setChatMetadata(chatId, { docsChatbotConversationId, }); + log.info('Docs chatbot created for chatId', chatId); } - const response = await docsChatbotAIService.addMessage({ + const response = await this._docsChatbotAIService.addMessage({ message: prompt, conversationId: docsChatbotConversationId, }); + log.info('Docs chatbot message sent', { + chatId, + docsChatbotConversationId, + docsChatbotMessageId: response.id, + }); + return { responseContent: response.content, responseReferences: response.references, + docsChatbotMessageId: response.id, }; } @@ -891,29 +861,21 @@ export default class ParticipantController { const chatId = ChatMetadataStore.getChatIdFromHistoryOrNewChatId( context.history ); - - let docsChatbotHasThrownError = false; let docsResult: { responseContent?: string; responseReferences?: Reference[]; + docsChatbotMessageId?: string; } = {}; - if (this._docsChatbotAIService) { - try { - docsResult = await this._handleDocsRequestWithChatbot({ - docsChatbotAIService: this._docsChatbotAIService, - prompt: request.prompt, - chatId, - }); - } catch (error) { - // If the docs chatbot API is not available, fall back to Copilot’s LLM and include - // the MongoDB documentation link for users to go to our documentation site directly. - docsChatbotHasThrownError = true; - log.error(error); - } - } - - if (!this._docsChatbotAIService || docsChatbotHasThrownError) { + try { + docsResult = await this._handleDocsRequestWithChatbot({ + prompt: request.prompt, + chatId, + }); + } catch (error) { + // If the docs chatbot API is not available, fall back to Copilot’s LLM and include + // the MongoDB documentation link for users to go to our documentation site directly. + log.error(error); docsResult = await this._handleDocsRequestWithCopilot(...args); } @@ -935,7 +897,10 @@ export default class ParticipantController { } } - return docsRequestChatResult(chatId); + return docsRequestChatResult({ + chatId, + docsChatbotMessageId: docsResult.docsChatbotMessageId, + }); } async chatHandler( @@ -990,7 +955,39 @@ export default class ParticipantController { } } - handleUserFeedback(feedback: vscode.ChatResultFeedback): void { + async _rateDocsChatbotMessage( + feedback: vscode.ChatResultFeedback + ): Promise { + const chatId = feedback.result.metadata?.chatId; + if (!chatId) { + return; + } + + const { docsChatbotConversationId } = + this._chatMetadataStore.getChatMetadata(chatId) ?? {}; + if ( + !docsChatbotConversationId || + !feedback.result.metadata?.docsChatbotMessageId + ) { + return; + } + + try { + const rating = await this._docsChatbotAIService.rateMessage({ + conversationId: docsChatbotConversationId, + messageId: feedback.result.metadata?.docsChatbotMessageId, + rating: !!feedback.kind, + }); + log.info('Docs chatbot rating sent', rating); + } catch (error) { + log.error(error); + } + } + + async handleUserFeedback(feedback: vscode.ChatResultFeedback): Promise { + if (feedback.result.metadata?.intent === 'docs') { + await this._rateDocsChatbotMessage(feedback); + } this._telemetryService.trackCopilotParticipantFeedback({ feedback: chatResultFeedbackKindToTelemetryValue(feedback.kind), reason: feedback.unhelpfulReason, diff --git a/src/test/suite/participant/docsChatbotAIService.test.ts b/src/test/suite/participant/docsChatbotAIService.test.ts index 6860c7a0b..827cf6841 100644 --- a/src/test/suite/participant/docsChatbotAIService.test.ts +++ b/src/test/suite/participant/docsChatbotAIService.test.ts @@ -6,103 +6,114 @@ import { DocsChatbotAIService } from '../../../participant/docsChatbotAIService' suite('DocsChatbotAIService Test Suite', function () { const initialFetch = global.fetch; + let docsChatbotAIService: DocsChatbotAIService; + + beforeEach(() => { + docsChatbotAIService = new DocsChatbotAIService(); + }); afterEach(function () { global.fetch = initialFetch; sinon.restore(); }); - suite('when serverBaseUri is missing', function () { - test('DocsChatbotAIService constructor does not throw', () => { - const docsChatbotAIService = new DocsChatbotAIService(); - expect(docsChatbotAIService._serverBaseUri).to.be.undefined; - }); - - test('createConversation throws if serverBaseUri is not set', async () => { - const docsChatbotAIService = new DocsChatbotAIService(); - try { - await docsChatbotAIService.createConversation(); - expect.fail('It must fail with missing serverBaseUri'); - } catch (error) { - expect((error as Error).message).to.include( - 'You must define a serverBaseUri for the DocsChatbotAIService' - ); - } + test('creates conversations', async () => { + const fetchStub = sinon.stub().resolves({ + status: 200, + ok: true, + json: () => + Promise.resolve({ + _id: '650b4b260f975ef031016c8a', + messages: [], + }), }); + global.fetch = fetchStub; + const conversation = await docsChatbotAIService.createConversation(); + expect(conversation._id).to.be.eql('650b4b260f975ef031016c8a'); }); - suite('when serverBaseUri is present', function () { - const serverBaseUri = 'https://example.com/'; - let docsChatbotAIService: DocsChatbotAIService; - - beforeEach(() => { - docsChatbotAIService = new DocsChatbotAIService(serverBaseUri); + test('throws on server errors', async () => { + const fetchStub = sinon.stub().resolves({ + status: 500, + ok: false, + statusText: 'Server error', + json: sinon.stub().rejects(new Error('invalid json')), }); + global.fetch = fetchStub; - test('creates conversations', async () => { - const fetchStub = sinon.stub().resolves({ - status: 200, - ok: true, - json: () => - Promise.resolve({ - _id: '650b4b260f975ef031016c8a', - messages: [], - }), - }); - global.fetch = fetchStub; - const conversation = await docsChatbotAIService.createConversation(); - expect(conversation._id).to.be.eql('650b4b260f975ef031016c8a'); + try { + await docsChatbotAIService.createConversation(); + expect.fail('It must fail with the server error'); + } catch (error) { + expect((error as Error).message).to.include('Internal server error'); + } + }); + + test('throws on bad requests', async () => { + const fetchStub = sinon.stub().resolves({ + status: 400, + ok: false, + statusText: 'Client error', + json: sinon.stub().resolves({}), }); + global.fetch = fetchStub; - test('throws on server errors', async () => { - const fetchStub = sinon.stub().resolves({ - status: 500, - ok: false, - statusText: 'Server error', - json: sinon.stub().rejects(new Error('invalid json')), - }); - global.fetch = fetchStub; + try { + await docsChatbotAIService.createConversation(); + expect.fail('It must fail with the bad request error'); + } catch (error) { + expect((error as Error).message).to.include('Bad request'); + } + }); - try { - await docsChatbotAIService.createConversation(); - expect.fail('It must fail with the server error'); - } catch (error) { - expect((error as Error).message).to.include('Internal server error'); - } + test('throws on a rate limit', async () => { + const fetchStub = sinon.stub().resolves({ + status: 429, + ok: false, + statusText: 'Model error', + json: sinon.stub().resolves({}), }); + global.fetch = fetchStub; - test('throws on bad requests', async () => { - const fetchStub = sinon.stub().resolves({ - status: 400, - ok: false, - statusText: 'Client error', - json: sinon.stub().resolves({}), - }); - global.fetch = fetchStub; + try { + await docsChatbotAIService.createConversation(); + expect.fail('It must fail with the rate limited error'); + } catch (error) { + expect((error as Error).message).to.include('Rate limited'); + } + }); - try { - await docsChatbotAIService.createConversation(); - expect.fail('It must fail with the bad request error'); - } catch (error) { - expect((error as Error).message).to.include('Bad request'); - } + test('throws on timeout', async () => { + const fetchStub = sinon.stub().resolves({ + status: 504, + ok: false, + json: sinon.stub().resolves({}), }); + global.fetch = fetchStub; - test('throws on a rate limit', async () => { - const fetchStub = sinon.stub().resolves({ - status: 429, - ok: false, - statusText: 'Model error', - json: sinon.stub().resolves({}), + try { + await docsChatbotAIService.addMessage({ + conversationId: '650b4b260f975ef031016c8a', + message: 'what is mongosh?', }); - global.fetch = fetchStub; + expect.fail('It must fail with the timeout error'); + } catch (error) { + expect((error as Error).message).to.include('Timeout'); + } + }); - try { - await docsChatbotAIService.createConversation(); - expect.fail('It must fail with the rate limited error'); - } catch (error) { - expect((error as Error).message).to.include('Rate limited'); - } + test('rates docs chatbot response', async () => { + const fetchStub = sinon.stub().resolves({ + status: 204, + ok: true, + json: () => Promise.resolve(true), + }); + global.fetch = fetchStub; + const rating = await docsChatbotAIService.rateMessage({ + conversationId: '650b4b260f975ef031016c8a', + messageId: '1', + rating: true, }); + expect(rating).to.be.eql(true); }); }); diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index be46143d2..aed2801cb 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1060,12 +1060,18 @@ suite('Participant Controller Test Suite', function () { const initialFetch = global.fetch; let fetchStub; + beforeEach(function () { + sendRequestStub.onCall(0).resolves({ + text: ['connection info'], + }); + }); + afterEach(function () { global.fetch = initialFetch; sinon.restore(); }); - beforeEach(function () { + test('uses docs chatbot result if available', async function () { fetchStub = sinon.stub().resolves({ status: 200, ok: true, @@ -1076,20 +1082,6 @@ suite('Participant Controller Test Suite', function () { }), }); global.fetch = fetchStub; - sendRequestStub.onCall(0).resolves({ - text: ['connection info'], - }); - }); - - test('uses docs chatbot when it is available', async function () { - sinon.replace( - testParticipantController, - '_readDocsChatbotBaseUri', - sinon.stub().resolves('url') - ); - await testParticipantController.createDocsChatbot( - extensionContextStub - ); const chatRequestMock = { prompt: 'how to connect to mongodb', command: 'docs', @@ -1100,14 +1092,20 @@ suite('Participant Controller Test Suite', function () { expect(sendRequestStub).to.have.not.been.called; }); - test('falls back to the copilot model when docs chatbot api is not available', async function () { + test('falls back to the copilot model when docs chatbot result is not available', async function () { + fetchStub = sinon.stub().resolves({ + status: 500, + ok: false, + statusText: 'Internal Server Error', + json: () => Promise.reject(new Error('invalid json')), + }); + global.fetch = fetchStub; const chatRequestMock = { prompt: 'how to connect to mongodb', command: 'docs', references: [], }; await invokeChatHandler(chatRequestMock); - expect(fetchStub).to.have.not.been.called; expect(sendRequestStub).to.have.been.called; }); }); @@ -1115,8 +1113,8 @@ suite('Participant Controller Test Suite', function () { }); suite('telemetry', function () { - test('reports positive user feedback', function () { - testParticipantController.handleUserFeedback({ + test('reports positive user feedback', async function () { + await testParticipantController.handleUserFeedback({ kind: vscode.ChatResultFeedbackKind.Helpful, result: { metadata: { @@ -1142,8 +1140,8 @@ suite('Participant Controller Test Suite', function () { .and.not.include('1234-5678-9012-3456'); }); - test('reports negative user feedback', function () { - testParticipantController.handleUserFeedback({ + test('reports negative user feedback', async function () { + await testParticipantController.handleUserFeedback({ kind: vscode.ChatResultFeedbackKind.Unhelpful, result: { metadata: { From 263b0433590546f4781146dce7b0f4a2dfcbb80a Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 23 Sep 2024 07:00:06 -0400 Subject: [PATCH 28/45] feat(chat): add /schema command handler VSCODE-571 (#820) --- package.json | 13 + src/commands/index.ts | 1 + src/mdbExtensionController.ts | 46 ++- src/participant/chatMetadata.ts | 7 +- src/participant/constants.ts | 6 + src/participant/markdown.ts | 20 +- src/participant/participant.ts | 387 ++++++++++++++---- src/participant/prompts/generic.ts | 4 + src/participant/prompts/history.ts | 13 + src/participant/prompts/schema.ts | 85 ++++ src/participant/schema.ts | 8 +- src/test/suite/mdbExtensionController.test.ts | 82 +++- .../suite/participant/participant.test.ts | 240 +++++++++-- 13 files changed, 750 insertions(+), 162 deletions(-) create mode 100644 src/participant/prompts/schema.ts diff --git a/package.json b/package.json index 61ea98fe2..d1d102828 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,11 @@ "name": "docs", "isSticky": true, "description": "Ask MongoDB-related questions and find answers in the official documentation." + }, + { + "name": "schema", + "isSticky": true, + "description": "Analyze a collection's schema." } ] } @@ -177,6 +182,10 @@ "command": "mdb.selectCollectionWithParticipant", "title": "MongoDB: Select Collection with Participant" }, + { + "command": "mdb.participantViewRawSchemaOutput", + "title": "MongoDB: View Raw Schema JSON Output" + }, { "command": "mdb.connectWithParticipant", "title": "MongoDB: Change Active Connection with Participant" @@ -747,6 +756,10 @@ "command": "mdb.selectCollectionWithParticipant", "when": "false" }, + { + "command": "mdb.participantViewRawSchemaOutput", + "when": "false" + }, { "command": "mdb.connectWithParticipant", "when": "false" diff --git a/src/commands/index.ts b/src/commands/index.ts index 2a11bf67e..767b99ffb 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -82,6 +82,7 @@ enum EXTENSION_COMMANDS { CONNECT_WITH_PARTICIPANT = 'mdb.connectWithParticipant', SELECT_DATABASE_WITH_PARTICIPANT = 'mdb.selectDatabaseWithParticipant', SELECT_COLLECTION_WITH_PARTICIPANT = 'mdb.selectCollectionWithParticipant', + PARTICIPANT_OPEN_RAW_SCHEMA_OUTPUT = 'mdb.participantViewRawSchemaOutput', } export default EXTENSION_COMMANDS; diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index a72aee212..7103183ee 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -42,8 +42,12 @@ import WebviewController from './views/webviewController'; import { createIdFactory, generateId } from './utils/objectIdHelper'; import { ConnectionStorage } from './storage/connectionStorage'; import type StreamProcessorTreeItem from './explorer/streamProcessorTreeItem'; -import type { RunParticipantQueryCommandArgs } from './participant/participant'; +import type { + ParticipantCommand, + RunParticipantQueryCommandArgs, +} from './participant/participant'; import ParticipantController from './participant/participant'; +import type { OpenSchemaCommandArgs } from './participant/prompts/schema'; // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. @@ -309,30 +313,38 @@ export default class MDBExtensionController implements vscode.Disposable { ); this.registerCommand( EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, - (id?: string) => - this._participantController.connectWithParticipant( - id ? decodeURIComponent(id) : id - ) + (data: { id?: string; command?: string }) => { + return this._participantController.connectWithParticipant(data); + } ); this.registerCommand( EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, - (_data: string) => { - const data = JSON.parse(decodeURIComponent(_data)); - return this._participantController.selectDatabaseWithParticipant({ - chatId: data.chatId, - databaseName: data.databaseName, - }); + (data: { + chatId: string; + command: ParticipantCommand; + databaseName?: string; + }) => { + return this._participantController.selectDatabaseWithParticipant(data); } ); this.registerCommand( EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, - (_data: string) => { - const data = JSON.parse(decodeURIComponent(_data)); - return this._participantController.selectCollectionWithParticipant({ - chatId: data.chatId, - databaseName: data.databaseName, - collectionName: data.collectionName, + (data: any) => { + return this._participantController.selectCollectionWithParticipant( + data + ); + } + ); + this.registerCommand( + EXTENSION_COMMANDS.PARTICIPANT_OPEN_RAW_SCHEMA_OUTPUT, + async ({ schema }: OpenSchemaCommandArgs) => { + const document = await vscode.workspace.openTextDocument({ + language: 'json', + content: schema, }); + await vscode.window.showTextDocument(document, { preview: true }); + + return !!document; } ); }; diff --git a/src/participant/chatMetadata.ts b/src/participant/chatMetadata.ts index dc31d28ae..1462c4ae4 100644 --- a/src/participant/chatMetadata.ts +++ b/src/participant/chatMetadata.ts @@ -20,6 +20,11 @@ export class ChatMetadataStore { return this._chats[chatId]; } + // Exposed for stubbing in tests. + static createNewChatId(): string { + return uuidv4(); + } + static getChatIdFromHistoryOrNewChatId( history: ReadonlyArray ): string { @@ -32,6 +37,6 @@ export class ChatMetadataStore { } } - return uuidv4(); + return ChatMetadataStore.createNewChatId(); } } diff --git a/src/participant/constants.ts b/src/participant/constants.ts index 59f80d4f4..b03e66274 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -100,3 +100,9 @@ export function docsRequestChatResult({ }, }; } + +export function schemaRequestChatResult( + history: ReadonlyArray +): ChatResult { + return createChatResult('schema', history); +} diff --git a/src/participant/markdown.ts b/src/participant/markdown.ts index df377b4a9..6684a0c9f 100644 --- a/src/participant/markdown.ts +++ b/src/participant/markdown.ts @@ -6,22 +6,14 @@ export function createMarkdownLink({ name, }: { commandId: string; - data?: - | { - [field: string]: any; - } - | string; + // TODO: Create types for this data so we can also then use them on the extension + // controller when we parse the result. + data: { + [field: string]: any; + }; name: string; }): vscode.MarkdownString { - const encodedData = data - ? encodeURIComponent( - `["${ - typeof data === 'string' - ? data - : encodeURIComponent(JSON.stringify(data)) - }"]` - ) - : undefined; + const encodedData = encodeURIComponent(JSON.stringify(data)); const commandQueryString = data ? `?${encodedData}` : ''; const link = new vscode.MarkdownString( `- ${name}\n` diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 5f2eb274d..4098014e9 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; -import { getSimplifiedSchema } from 'mongodb-schema'; +import { getSimplifiedSchema, parseSchema } from 'mongodb-schema'; import type { Document } from 'bson'; +import type { Reference } from 'mongodb-rag-core'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; @@ -8,7 +9,7 @@ import type { LoadedConnection } from '../storage/connectionStorage'; import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; -import { GenericPrompt } from './prompts/generic'; +import { GenericPrompt, isPromptEmpty } from './prompts/generic'; import type { ChatResult } from './constants'; import { askToConnectChatResult, @@ -18,6 +19,7 @@ import { namespaceRequestChatResult, queryRequestChatResult, docsRequestChatResult, + schemaRequestChatResult, } from './constants'; import { QueryPrompt } from './prompts/query'; import { COL_NAME_ID, DB_NAME_ID, NamespacePrompt } from './prompts/namespace'; @@ -26,14 +28,19 @@ import { getSimplifiedSampleDocuments } from './sampleDocuments'; import { getCopilotModel } from './model'; import { createMarkdownLink } from './markdown'; import { ChatMetadataStore } from './chatMetadata'; -import { DocsChatbotAIService } from './docsChatbotAIService'; +import { doesLastMessageAskForNamespace } from './prompts/history'; +import { + DOCUMENTS_TO_SAMPLE_FOR_SCHEMA_PROMPT, + type OpenSchemaCommandArgs, + SchemaPrompt, +} from './prompts/schema'; import { chatResultFeedbackKindToTelemetryValue, ParticipantErrorTypes, TelemetryEventTypes, } from '../telemetry/telemetryService'; +import { DocsChatbotAIService } from './docsChatbotAIService'; import type TelemetryService from '../telemetry/telemetryService'; -import type { Reference } from 'mongodb-rag-core'; const log = createLogger('participant'); @@ -53,6 +60,8 @@ export type RunParticipantQueryCommandArgs = { const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)`; const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)`; +export type ParticipantCommand = '/query' | '/schema' | '/docs'; + const MAX_MARKDOWN_LIST_LENGTH = 10; export function parseForDatabaseAndCollectionName(text: string): { @@ -250,7 +259,13 @@ export default class ParticipantController { return genericRequestChatResult(context.history); } - async connectWithParticipant(id?: string): Promise { + async connectWithParticipant({ + id, + command, + }: { + id?: string; + command?: string; + }): Promise { if (!id) { const didChangeActiveConnection = await this._connectionController.changeActiveConnection(); @@ -265,11 +280,11 @@ export default class ParticipantController { const connectionName = this._connectionController.getActiveConnectionName(); return this.writeChatMessageAsUser( - `/query ${connectionName}` + `${command ? `${command} ` : ''}${connectionName}` ) as Promise; } - getConnectionsTree(): vscode.MarkdownString[] { + getConnectionsTree(command: ParticipantCommand): vscode.MarkdownString[] { return [ ...this._connectionController .getSavedConnections() @@ -282,22 +297,30 @@ export default class ParticipantController { .map((conn: LoadedConnection) => createMarkdownLink({ commandId: EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, - data: conn.id, + data: { + id: conn.id, + command, + }, name: conn.name, }) ), createMarkdownLink({ commandId: EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, name: 'Show more', + data: { + command, + }, }), ]; } - async getDatabaseQuickPicks(): Promise { + async getDatabaseQuickPicks( + command: ParticipantCommand + ): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { // Run a blank command to get the user to connect first. - void this.writeChatMessageAsUser('/query'); + void this.writeChatMessageAsUser(command); return []; } @@ -314,8 +337,10 @@ export default class ParticipantController { } } - async _selectDatabaseWithQuickPick(): Promise { - const databases = await this.getDatabaseQuickPicks(); + async _selectDatabaseWithQuickPick( + command: ParticipantCommand + ): Promise { + const databases = await this.getDatabaseQuickPicks(command); const selectedQuickPickItem = await vscode.window.showQuickPick(databases, { placeHolder: 'Select a database...', }); @@ -324,14 +349,16 @@ export default class ParticipantController { async selectDatabaseWithParticipant({ chatId, + command, databaseName: _databaseName, }: { chatId: string; + command: ParticipantCommand; databaseName?: string; }): Promise { let databaseName: string | undefined = _databaseName; if (!databaseName) { - databaseName = await this._selectDatabaseWithQuickPick(); + databaseName = await this._selectDatabaseWithQuickPick(command); if (!databaseName) { return false; } @@ -342,17 +369,21 @@ export default class ParticipantController { }); return this.writeChatMessageAsUser( - `/query ${databaseName}` + `${command} ${databaseName}` ) as Promise; } - async getCollectionQuickPicks( - databaseName: string - ): Promise { + async getCollectionQuickPicks({ + command, + databaseName, + }: { + command: ParticipantCommand; + databaseName: string; + }): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { // Run a blank command to get the user to connect first. - void this.writeChatMessageAsUser('/query'); + void this.writeChatMessageAsUser(command); return []; } @@ -367,10 +398,17 @@ export default class ParticipantController { } } - async _selectCollectionWithQuickPick( - databaseName: string - ): Promise { - const collections = await this.getCollectionQuickPicks(databaseName); + async _selectCollectionWithQuickPick({ + command, + databaseName, + }: { + command: ParticipantCommand; + databaseName: string; + }): Promise { + const collections = await this.getCollectionQuickPicks({ + command, + databaseName, + }); const selectedQuickPickItem = await vscode.window.showQuickPick( collections, { @@ -381,17 +419,22 @@ export default class ParticipantController { } async selectCollectionWithParticipant({ + command, chatId, databaseName, collectionName: _collectionName, }: { + command: ParticipantCommand; chatId: string; databaseName: string; collectionName?: string; }): Promise { let collectionName: string | undefined = _collectionName; if (!collectionName) { - collectionName = await this._selectCollectionWithQuickPick(databaseName); + collectionName = await this._selectCollectionWithQuickPick({ + command, + databaseName, + }); if (!collectionName) { return false; } @@ -402,13 +445,17 @@ export default class ParticipantController { collectionName: collectionName, }); return this.writeChatMessageAsUser( - `/query ${collectionName}` + `${command} ${collectionName}` ) as Promise; } - async getDatabasesTree( - context: vscode.ChatContext - ): Promise { + async getDatabasesTree({ + command, + context, + }: { + command: ParticipantCommand; + context: vscode.ChatContext; + }): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { return []; @@ -423,6 +470,7 @@ export default class ParticipantController { createMarkdownLink({ commandId: EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, data: { + command, chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( context.history ), @@ -435,6 +483,7 @@ export default class ParticipantController { ? [ createMarkdownLink({ data: { + command, chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( context.history ), @@ -451,10 +500,15 @@ export default class ParticipantController { } } - async getCollectionTree( - databaseName: string, - context: vscode.ChatContext - ): Promise { + async getCollectionTree({ + command, + context, + databaseName, + }: { + command: ParticipantCommand; + databaseName: string; + context: vscode.ChatContext; + }): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { return []; @@ -467,6 +521,7 @@ export default class ParticipantController { createMarkdownLink({ commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, data: { + command, chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( context.history ), @@ -482,9 +537,11 @@ export default class ParticipantController { commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, data: { + command, chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( context.history ), + databaseName, }, name: 'Show more', }), @@ -541,11 +598,13 @@ export default class ParticipantController { } async _askForNamespace({ + command, context, databaseName, collectionName, stream, }: { + command: ParticipantCommand; context: vscode.ChatContext; databaseName: string | undefined; collectionName: string | undefined; @@ -555,17 +614,26 @@ export default class ParticipantController { // we retrieve the available namespaces from the current connection. // Users can then select a value by clicking on an item in the list. if (!databaseName) { - const tree = await this.getDatabasesTree(context); + const tree = await this.getDatabasesTree({ + command, + context, + }); stream.markdown( - 'What is the name of the database you would like this query to run against?\n\n' + `What is the name of the database you would like${ + command === '/query' ? ' this query' : '' + } to run against?\n\n` ); for (const item of tree) { stream.markdown(item); } } else if (!collectionName) { - const tree = await this.getCollectionTree(databaseName, context); + const tree = await this.getCollectionTree({ + command, + databaseName, + context, + }); stream.markdown( - `Which collection would you like to query within ${databaseName}?\n\n` + `Which collection would you like to use within ${databaseName}?\n\n` ); for (const item of tree) { stream.markdown(item); @@ -579,15 +647,20 @@ export default class ParticipantController { }); } - _askToConnect( - context: vscode.ChatContext, - stream: vscode.ChatResponseStream - ): ChatResult { - const tree = this.getConnectionsTree(); + _askToConnect({ + command, + context, + stream, + }: { + command: ParticipantCommand; + context: vscode.ChatContext; + stream: vscode.ChatResponseStream; + }): ChatResult { stream.markdown( "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against.\n\n" ); + const tree = this.getConnectionsTree(command); for (const item of tree) { stream.markdown(item); } @@ -595,39 +668,60 @@ export default class ParticipantController { } // The sample documents returned from this are simplified (strings and arrays shortened). + // The sample documents are only returned when a user has the setting enabled. async _fetchCollectionSchemaAndSampleDocuments({ abortSignal, databaseName, collectionName, + amountOfDocumentsToSample = NUM_DOCUMENTS_TO_SAMPLE, + schemaFormat = 'simplified', }: { abortSignal; databaseName: string; collectionName: string; + amountOfDocumentsToSample?: number; + schemaFormat?: 'simplified' | 'full'; }): Promise<{ schema?: string; sampleDocuments?: Document[]; + amountOfDocumentsSampled: number; }> { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { - return {}; + return { + amountOfDocumentsSampled: 0, + }; } try { - const sampleDocuments = - (await dataService?.sample?.( - `${databaseName}.${collectionName}`, - { - query: {}, - size: NUM_DOCUMENTS_TO_SAMPLE, - }, - { promoteValues: false }, - { - abortSignal, - } - )) || []; + const sampleDocuments = await dataService.sample( + `${databaseName}.${collectionName}`, + { + query: {}, + size: amountOfDocumentsToSample, + }, + { promoteValues: false }, + { + abortSignal, + } + ); - const unformattedSchema = await getSimplifiedSchema(sampleDocuments); - const schema = new SchemaFormatter().format(unformattedSchema); + if (!sampleDocuments) { + return { + amountOfDocumentsSampled: 0, + }; + } + + let schema: string; + if (schemaFormat === 'simplified') { + const unformattedSchema = await getSimplifiedSchema(sampleDocuments); + schema = new SchemaFormatter().format(unformattedSchema); + } else { + const unformattedSchema = await parseSchema(sampleDocuments, { + storeValues: false, + }); + schema = JSON.stringify(unformattedSchema, null, 2); + } const useSampleDocsInCopilot = !!vscode.workspace .getConfiguration('mdb') @@ -638,17 +732,20 @@ export default class ParticipantController { ? getSimplifiedSampleDocuments(sampleDocuments) : undefined, schema, + amountOfDocumentsSampled: sampleDocuments.length, }; } catch (err: any) { log.error('Unable to fetch schema and sample documents', err); - return {}; + throw err; } } - async handleEmptyQueryRequest({ + async handleEmptyNamespaceMessage({ + command, context, stream, }: { + command: ParticipantCommand; context: vscode.ChatContext; stream: vscode.ChatResponseStream; }): Promise { @@ -670,14 +767,21 @@ export default class ParticipantController { 'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.' ) ); - tree = await this.getCollectionTree(databaseName, context); + tree = await this.getCollectionTree({ + command, + databaseName, + context, + }); } else { stream.markdown( vscode.l10n.t( 'Please select a database by either clicking on an item in the list or typing the name manually in the chat.' ) ); - tree = await this.getDatabasesTree(context); + tree = await this.getDatabasesTree({ + command, + context, + }); } for (const item of tree) { @@ -691,28 +795,32 @@ export default class ParticipantController { }); } - // @MongoDB /query find all documents where the "address" has the word Broadway in it. - async handleQueryRequest( + // @MongoDB /schema + async handleSchemaRequest( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken ): Promise { if (!this._connectionController.getActiveDataService()) { - return this._askToConnect(context, stream); + return this._askToConnect({ + command: '/schema', + context, + stream, + }); } - if (!request.prompt || request.prompt.trim().length === 0) { - return this.handleEmptyQueryRequest({ + if ( + isPromptEmpty(request) && + doesLastMessageAskForNamespace(context.history) + ) { + return this.handleEmptyNamespaceMessage({ + command: '/schema', context, stream, }); } - // We "prompt chain" to handle the query requests. - // First we ask the model to parse for the database and collection name. - // If they exist, we can then use them in our final completion. - // When they don't exist we ask the user for them. const { databaseName, collectionName } = await this._getNamespaceFromChat({ request, context, @@ -720,6 +828,7 @@ export default class ParticipantController { }); if (!databaseName || !collectionName) { return await this._askForNamespace({ + command: '/schema', context, databaseName, collectionName, @@ -732,12 +841,147 @@ export default class ParticipantController { abortController.abort(); }); - const { schema, sampleDocuments } = - await this._fetchCollectionSchemaAndSampleDocuments({ + stream.push( + new vscode.ChatResponseProgressPart( + 'Fetching documents and analyzing schema...' + ) + ); + + let sampleDocuments: Document[] | undefined; + let amountOfDocumentsSampled: number; + let schema: string | undefined; + try { + ({ + sampleDocuments, + amountOfDocumentsSampled, // There can be fewer than the amount we attempt to sample. + schema, + } = await this._fetchCollectionSchemaAndSampleDocuments({ abortSignal: abortController.signal, databaseName, + schemaFormat: 'full', collectionName, + amountOfDocumentsToSample: DOCUMENTS_TO_SAMPLE_FOR_SCHEMA_PROMPT, + })); + + if (!schema || amountOfDocumentsSampled === 0) { + stream.markdown( + vscode.l10n.t( + 'Unable to generate a schema from the collection, no documents found.' + ) + ); + return schemaRequestChatResult(context.history); + } + } catch (e) { + stream.markdown( + vscode.l10n.t( + `Unable to generate a schema from the collection, an error occurred: ${e}` + ) + ); + return schemaRequestChatResult(context.history); + } + + const messages = SchemaPrompt.buildMessages({ + request, + context, + databaseName, + amountOfDocumentsSampled, + collectionName, + schema, + connectionNames: this._connectionController + .getSavedConnections() + .map((connection) => connection.name), + ...(sampleDocuments ? { sampleDocuments } : {}), + }); + const responseContent = await this.getChatResponseContent({ + messages, + token, + }); + stream.markdown(responseContent); + + stream.button({ + command: EXTENSION_COMMANDS.PARTICIPANT_OPEN_RAW_SCHEMA_OUTPUT, + title: vscode.l10n.t('Open JSON Output'), + arguments: [ + { + schema, + } as OpenSchemaCommandArgs, + ], + }); + + return schemaRequestChatResult(context.history); + } + + // @MongoDB /query find all documents where the "address" has the word Broadway in it. + async handleQueryRequest( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ): Promise { + if (!this._connectionController.getActiveDataService()) { + return this._askToConnect({ + command: '/query', + context, + stream, }); + } + + if (isPromptEmpty(request)) { + if (doesLastMessageAskForNamespace(context.history)) { + return this.handleEmptyNamespaceMessage({ + command: '/query', + context, + stream, + }); + } + + stream.markdown(QueryPrompt.getEmptyRequestResponse()); + return emptyRequestChatResult(context.history); + } + + // We "prompt chain" to handle the query requests. + // First we ask the model to parse for the database and collection name. + // If they exist, we can then use them in our final completion. + // When they don't exist we ask the user for them. + const { databaseName, collectionName } = await this._getNamespaceFromChat({ + request, + context, + token, + }); + if (!databaseName || !collectionName) { + return await this._askForNamespace({ + command: '/query', + context, + databaseName, + collectionName, + stream, + }); + } + + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); + + let schema: string | undefined; + let sampleDocuments: Document[] | undefined; + try { + ({ schema, sampleDocuments } = + await this._fetchCollectionSchemaAndSampleDocuments({ + abortSignal: abortController.signal, + databaseName, + collectionName, + })); + } catch (e) { + // When an error fetching the collection schema or sample docs occurs, + // we still want to continue as it isn't critical, however, + // we do want to notify the user. + stream.markdown( + vscode.l10n.t( + 'An error occurred while fetching the collection schema and sample documents.\nThe generated query will not be able to reference your data.' + ) + ); + } const messages = await QueryPrompt.buildMessages({ request, @@ -940,8 +1184,7 @@ export default class ParticipantController { case 'docs': return await this.handleDocsRequest(...args); case 'schema': - // TODO(VSCODE-571): Implement this. - return await this.handleGenericRequest(...args); + return await this.handleSchemaRequest(...args); default: if (!request.prompt?.trim()) { stream.markdown(GenericPrompt.getEmptyRequestResponse()); diff --git a/src/participant/prompts/generic.ts b/src/participant/prompts/generic.ts index d176fead2..733a86fe9 100644 --- a/src/participant/prompts/generic.ts +++ b/src/participant/prompts/generic.ts @@ -46,3 +46,7 @@ Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; return messages; } } + +export function isPromptEmpty(request: vscode.ChatRequest): boolean { + return !request.prompt || request.prompt.trim().length === 0; +} diff --git a/src/participant/prompts/history.ts b/src/participant/prompts/history.ts index 58223876e..4f3bfe25d 100644 --- a/src/participant/prompts/history.ts +++ b/src/participant/prompts/history.ts @@ -67,3 +67,16 @@ export function getHistoryMessages({ return messages; } + +export function doesLastMessageAskForNamespace( + history: ReadonlyArray +): boolean { + const lastMessageMetaData: vscode.ChatResponseTurn | undefined = history[ + history.length - 1 + ] as vscode.ChatResponseTurn; + + return ( + (lastMessageMetaData?.result as ChatResult)?.metadata?.intent === + 'askForNamespace' + ); +} diff --git a/src/participant/prompts/schema.ts b/src/participant/prompts/schema.ts new file mode 100644 index 000000000..ffbe6ba61 --- /dev/null +++ b/src/participant/prompts/schema.ts @@ -0,0 +1,85 @@ +import * as vscode from 'vscode'; + +import { getHistoryMessages } from './history'; + +export const DOCUMENTS_TO_SAMPLE_FOR_SCHEMA_PROMPT = 100; + +export type OpenSchemaCommandArgs = { + schema: string; +}; + +export class SchemaPrompt { + static getAssistantPrompt({ + amountOfDocumentsSampled, + }: { + amountOfDocumentsSampled: number; + }): vscode.LanguageModelChatMessage { + const prompt = `You are a senior engineer who describes the schema of documents in a MongoDB database. +The schema is generated from a sample of documents in the user's collection. +You must follows these rules. +Rule 1: Try to be as concise as possible. +Rule 2: Pay attention to the JSON schema. +Rule 3: Mention the amount of documents sampled in your response. +Amount of documents sampled: ${amountOfDocumentsSampled}.`; + + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.Assistant(prompt); + } + + static getUserPrompt({ + databaseName, + collectionName, + prompt, + schema, + }: { + databaseName: string; + collectionName: string; + prompt: string; + schema: string; + }): vscode.LanguageModelChatMessage { + const userInput = `${ + prompt ? `The user provided additional information: "${prompt}"\n` : '' + }Database name: ${databaseName} +Collection name: ${collectionName} +Schema: +${schema}`; + + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.User(userInput); + } + + static buildMessages({ + context, + databaseName, + collectionName, + schema, + amountOfDocumentsSampled, + request, + connectionNames, + }: { + request: { + prompt: string; + }; + databaseName: string; + collectionName: string; + schema: string; + amountOfDocumentsSampled: number; + context: vscode.ChatContext; + connectionNames: string[]; + }): vscode.LanguageModelChatMessage[] { + const messages = [ + SchemaPrompt.getAssistantPrompt({ + amountOfDocumentsSampled, + }), + ...getHistoryMessages({ context, connectionNames }), + SchemaPrompt.getUserPrompt({ + prompt: request.prompt, + databaseName, + collectionName, + schema, + }), + ]; + + return messages; + } +} diff --git a/src/participant/schema.ts b/src/participant/schema.ts index faf1e7fb8..ad247eb70 100644 --- a/src/participant/schema.ts +++ b/src/participant/schema.ts @@ -22,13 +22,13 @@ export class SchemaFormatter { private processSchemaTypeList( prefix: string, pTypes: SimplifiedSchemaType[] - ) { + ): void { if (pTypes.length !== 0) { this.processSchemaType(prefix, pTypes[0]); } } - private processSchemaType(prefix: string, pType: SimplifiedSchemaType) { + private processSchemaType(prefix: string, pType: SimplifiedSchemaType): void { const bsonType = pType.bsonType; if (bsonType === 'Document') { const fields = (pType as SimplifiedSchemaDocumentType).fields; @@ -67,7 +67,7 @@ export class SchemaFormatter { this.addToFormattedSchemaString(prefix + ': ' + bsonType); } - private processDocumentType(prefix: string, pDoc: SimplifiedSchema) { + private processDocumentType(prefix: string, pDoc: SimplifiedSchema): void { if (!pDoc) { return; } @@ -93,7 +93,7 @@ export class SchemaFormatter { } } - addToFormattedSchemaString(fieldAndType: string) { + addToFormattedSchemaString(fieldAndType: string): void { if (this.schemaString.length > 0) { this.schemaString += '\n'; } diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index e887485b9..5e11b1cc3 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -33,7 +33,7 @@ const testDatabaseURI = 'mongodb://localhost:27088'; function getTestConnectionTreeItem( options?: Partial[0]> -) { +): ConnectionTreeItem { return new ConnectionTreeItem({ connectionId: 'tasty_sandwhich', collapsibleState: vscode.TreeItemCollapsibleState.None, @@ -48,7 +48,7 @@ function getTestConnectionTreeItem( function getTestCollectionTreeItem( options?: Partial[0]> -) { +): CollectionTreeItem { return new CollectionTreeItem({ collection: { name: 'testColName', @@ -65,7 +65,7 @@ function getTestCollectionTreeItem( function getTestDatabaseTreeItem( options?: Partial[0]> -) { +): DatabaseTreeItem { return new DatabaseTreeItem({ databaseName: 'zebra', dataService: {} as DataService, @@ -78,7 +78,7 @@ function getTestDatabaseTreeItem( function getTestStreamProcessorTreeItem( options?: Partial[0]> -) { +): StreamProcessorTreeItem { return new StreamProcessorTreeItem({ streamProcessorName: 'zebra', streamProcessorState: 'CREATED', @@ -88,7 +88,7 @@ function getTestStreamProcessorTreeItem( }); } -function getTestFieldTreeItem() { +function getTestFieldTreeItem(): FieldTreeItem { return new FieldTreeItem({ field: { name: 'dolphins are sentient', @@ -101,7 +101,7 @@ function getTestFieldTreeItem() { }); } -function getTestSchemaTreeItem() { +function getTestSchemaTreeItem(): SchemaTreeItem { return new SchemaTreeItem({ databaseName: 'zebraWearwolf', collectionName: 'giraffeVampire', @@ -116,7 +116,7 @@ function getTestSchemaTreeItem() { function getTestDocumentTreeItem( options?: Partial[0]> -) { +): DocumentTreeItem { return new DocumentTreeItem({ document: {}, namespace: 'waffle.house', @@ -129,10 +129,14 @@ function getTestDocumentTreeItem( suite('MDBExtensionController Test Suite', function () { this.timeout(10000); + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + }); suite('when not connected', () => { let showErrorMessageStub: SinonSpy; - const sandbox = sinon.createSandbox(); beforeEach(() => { sandbox.stub(vscode.window, 'showInformationMessage'); @@ -145,10 +149,6 @@ suite('MDBExtensionController Test Suite', function () { ); }); - afterEach(() => { - sandbox.restore(); - }); - test('mdb.addDatabase command fails when not connected to the connection', async () => { const testTreeItem = getTestConnectionTreeItem(); const addDatabaseSucceeded = await vscode.commands.executeCommand( @@ -177,8 +177,6 @@ suite('MDBExtensionController Test Suite', function () { let fakeCreatePlaygroundFileWithContent: SinonSpy; let openExternalStub: SinonStub; - const sandbox = sinon.createSandbox(); - beforeEach(() => { showInformationMessageStub = sandbox.stub( vscode.window, @@ -206,10 +204,6 @@ suite('MDBExtensionController Test Suite', function () { ); }); - afterEach(() => { - sandbox.restore(); - }); - test('mdb.viewCollectionDocuments command should call onViewCollectionDocuments on the editor controller with the collection namespace', async () => { const textCollectionTree = getTestCollectionTreeItem(); await vscode.commands.executeCommand( @@ -1853,4 +1847,56 @@ suite('MDBExtensionController Test Suite', function () { }); }); }); + + test('mdb.participantViewRawSchemaOutput command opens a json document with the output', async () => { + const openTextDocumentStub = sandbox.stub( + vscode.workspace, + 'openTextDocument' + ); + const showTextDocumentStub = sandbox.stub( + vscode.window, + 'showTextDocument' + ); + + const schemaContent = `{ + "count": 1, + "fields": [ + { + "name": "_id", + "path": [ + "_id" + ], + "count": 1, + "type": "ObjectId", + "probability": 1, + "hasDuplicates": false, + "types": [ + { + "name": "ObjectId", + "path": [ + "_id" + ], + "count": 1, + "probability": 1, + "bsonType": "ObjectId" + } + ] + } + ] +}`; + await vscode.commands.executeCommand('mdb.participantViewRawSchemaOutput', { + schema: schemaContent, + }); + + assert(openTextDocumentStub.calledOnce); + assert.deepStrictEqual(openTextDocumentStub.firstCall.args[0], { + language: 'json', + content: schemaContent, + }); + + assert(showTextDocumentStub.calledOnce); + assert.deepStrictEqual(showTextDocumentStub.firstCall.args[1], { + preview: true, + }); + }); }); diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index aed2801cb..53172b106 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -25,6 +25,7 @@ import { StorageLocation, } from '../../../storage/storageController'; import type { LoadedConnection } from '../../../storage/connectionStorage'; +import { ChatMetadataStore } from '../../../participant/chatMetadata'; // The Copilot's model in not available in tests, // therefore we need to mock its methods and returning values. @@ -38,6 +39,12 @@ const loadedConnection = { connectionOptions: { connectionString: 'mongodb://localhost' }, }; +const testChatId = 'test-chat-id'; + +const encodeStringify = (obj: Record): string => { + return encodeURIComponent(JSON.stringify(obj)); +}; + suite('Participant Controller Test Suite', function () { const extensionContextStub = new ExtensionContextStub(); @@ -51,6 +58,7 @@ suite('Participant Controller Test Suite', function () { let testParticipantController: ParticipantController; let chatContextStub: vscode.ChatContext; let chatStreamStub: { + push: sinon.SinonSpy; markdown: sinon.SinonSpy; button: sinon.SinonSpy; }; @@ -84,6 +92,7 @@ suite('Participant Controller Test Suite', function () { storageController: testStorageController, telemetryService: testTelemetryService, }); + sinon.replace(ChatMetadataStore, 'createNewChatId', () => testChatId); testParticipantController = new ParticipantController({ connectionController: testConnectionController, storageController: testStorageController, @@ -100,6 +109,7 @@ suite('Participant Controller Test Suite', function () { ], }; chatStreamStub = { + push: sinon.fake(), markdown: sinon.fake(), button: sinon.fake(), }; @@ -205,14 +215,17 @@ suite('Participant Controller Test Suite', function () { "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against." ); const listConnectionsMessage = chatStreamStub.markdown.getCall(1).args[0]; + const expectedContent = encodeStringify({ id: 'id', command: '/query' }); expect(listConnectionsMessage.value).to.include( - '- localhost' + `- localhost` ); const showMoreMessage = chatStreamStub.markdown.getCall(2).args[0]; expect(showMoreMessage.value).to.include( - '- Show more' + `- Show more` ); - expect(chatResult?.metadata?.chatId.length).to.equal(36); + expect(chatResult?.metadata?.chatId.length).to.equal(testChatId.length); expect({ ...chatResult?.metadata, chatId: undefined, @@ -243,15 +256,18 @@ suite('Participant Controller Test Suite', function () { "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against." ); const listConnectionsMessage = chatStreamStub.markdown.getCall(1).args[0]; + const expectedContent = encodeStringify({ id: 'id0', command: '/query' }); expect(listConnectionsMessage.value).to.include( - '- localhost0' + `- localhost0` ); const showMoreMessage = chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreMessage.value).to.include( - '- Show more' + `- Show more` ); expect(chatStreamStub.markdown.callCount).to.be.eql(12); - expect(chatResult?.metadata?.chatId.length).to.equal(36); + expect(chatResult?.metadata?.chatId.length).to.equal(testChatId.length); expect({ ...chatResult?.metadata, chatId: undefined, @@ -278,14 +294,17 @@ suite('Participant Controller Test Suite', function () { "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against" ); const listConnectionsMessage = chatStreamStub.markdown.getCall(4).args[0]; + const expectedContent = encodeStringify({ id: 'id', command: '/query' }); expect(listConnectionsMessage.value).to.include( - '- localhost' + `- localhost` ); const showMoreMessage = chatStreamStub.markdown.getCall(5).args[0]; expect(showMoreMessage.value).to.include( - '- Show more' + `- Show more` ); - expect(chatResult?.metadata?.chatId.length).to.equal(36); + expect(chatResult?.metadata?.chatId.length).to.equal(testChatId.length); expect({ ...chatResult?.metadata, chatId: undefined, @@ -296,12 +315,14 @@ suite('Participant Controller Test Suite', function () { }); test('calls connect by id for an existing connection', async function () { - await testParticipantController.connectWithParticipant('123'); + await testParticipantController.connectWithParticipant({ + id: '123', + }); expect(connectWithConnectionIdStub).to.have.been.calledWithExactly('123'); }); test('calls connect with uri for a new connection', async function () { - await testParticipantController.connectWithParticipant(); + await testParticipantController.connectWithParticipant({}); expect(changeActiveConnectionStub).to.have.been.called; }); }); @@ -711,21 +732,27 @@ suite('Participant Controller Test Suite', function () { 'What is the name of the database you would like this query to run against?' ); const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; + const expectedContent = encodeStringify({ + command: '/query', + chatId: testChatId, + databaseName: 'dbOne', + }); expect(listDBsMessage.value).to.include( - '- dbOne' + `- dbOne` ); const showMoreDBsMessage = chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreDBsMessage.value).to.include( - '- Show more` ); - expect(showMoreDBsMessage.value).to.include('">Show more'); + expect(showMoreDBsMessage.value).to.include('"'); expect(chatStreamStub.markdown.callCount).to.be.eql(12); const firstChatId = chatResult?.metadata?.chatId; - expect(chatResult?.metadata?.chatId.length).to.equal(36); + expect(chatResult?.metadata?.chatId.length).to.equal( + testChatId.length + ); expect({ ...chatResult?.metadata, chatId: undefined, @@ -778,22 +805,30 @@ suite('Participant Controller Test Suite', function () { const askForCollMessage = chatStreamStub.markdown.getCall(12).args[0]; expect(askForCollMessage).to.include( - 'Which collection would you like to query within dbOne?' + 'Which collection would you like to use within dbOne?' ); const listCollsMessage = chatStreamStub.markdown.getCall(13).args[0]; + const expectedCollsContent = encodeStringify({ + command: '/query', + chatId: testChatId, + databaseName: 'dbOne', + collectionName: 'collOne', + }); expect(listCollsMessage.value).to.include( - '- collOne' + `- collOne` ); const showMoreCollsMessage = chatStreamStub.markdown.getCall(23).args[0]; expect(showMoreCollsMessage.value).to.include( - '- Show more` ); - expect(showMoreCollsMessage.value).to.include('">Show more'); expect(chatStreamStub.markdown.callCount).to.be.eql(24); expect(chatResult2?.metadata?.chatId).to.equal(firstChatId); expect({ @@ -939,15 +974,23 @@ suite('Participant Controller Test Suite', function () { ); const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; expect(listDBsMessage.value).to.include( - '- dbOne' + `- dbOne` ); const showMoreDBsMessage = chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreDBsMessage.value).to.include( - '- Show more` ); expect({ ...chatResult?.metadata, @@ -1033,15 +1076,25 @@ suite('Participant Controller Test Suite', function () { ); const listCollsMessage = chatStreamStub.markdown.getCall(1).args[0]; expect(listCollsMessage.value).to.include( - '- collOne' + `- collOne` ); const showMoreCollsMessage = - chatStreamStub.markdown.getCall(1).args[0]; + chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreCollsMessage.value).to.include( - '- Show more` ); expect({ ...chatResult?.metadata, @@ -1056,6 +1109,121 @@ suite('Participant Controller Test Suite', function () { }); }); + suite('schema command', function () { + suite('known namespace from running namespace LLM', function () { + beforeEach(function () { + sendRequestStub.onCall(0).resolves({ + text: ['DATABASE_NAME: dbOne\n', 'COLLECTION_NAME: collOne\n`'], + }); + }); + + test('shows a button to view the json output', async function () { + const chatRequestMock = { + prompt: '', + command: 'schema', + references: [], + }; + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203660'), + }, + ]); + await invokeChatHandler(chatRequestMock); + const expectedSchema = `{ + "count": 1, + "fields": [ + { + "name": "_id", + "path": [ + "_id" + ], + "count": 1, + "type": "ObjectId", + "probability": 1, + "hasDuplicates": false, + "types": [ + { + "name": "ObjectId", + "path": [ + "_id" + ], + "count": 1, + "probability": 1, + "bsonType": "ObjectId" + } + ] + } + ] +}`; + expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ + command: 'mdb.participantViewRawSchemaOutput', + title: 'Open JSON Output', + arguments: [ + { + schema: expectedSchema, + }, + ], + }); + }); + + test("includes the collection's schema in the request", async function () { + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203660'), + field: { + stringField: + 'There was a house cat who finally got the chance to do what it had always wanted to do.', + arrayField: [new Int32('1')], + }, + }, + { + _id: new ObjectId('63ed1d522d8573fa5c203660'), + field: { + stringField: 'Pineapple.', + arrayField: [new Int32('166')], + }, + }, + ]); + const chatRequestMock = { + prompt: '', + command: 'schema', + references: [], + }; + await invokeChatHandler(chatRequestMock); + const messages = sendRequestStub.secondCall.args[0]; + expect(messages[0].content).to.include( + 'Amount of documents sampled: 2' + ); + expect(messages[1].content).to.include( + `Database name: dbOne +Collection name: collOne +Schema: +{ + "count": 2, + "fields": [` + ); + expect(messages[1].content).to.include(`"name": "arrayField", + "path": [ + "field", + "arrayField" + ],`); + }); + + test('prints a message when no documents are found', async function () { + sampleStub.resolves([]); + const chatRequestMock = { + prompt: '', + command: 'schema', + references: [], + }; + await invokeChatHandler(chatRequestMock); + expect(chatStreamStub?.markdown.getCall(0).args[0]).to.include( + 'Unable to generate a schema from the collection, no documents found.' + ); + }); + }); + }); + suite('docs command', function () { const initialFetch = global.fetch; let fetchStub; From 4902f049fd29ef811f77b010ea3267e0f478c025 Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 23 Sep 2024 13:11:50 -0400 Subject: [PATCH 29/45] chore(participant): update anchor rendering to use markdown link instead of html a tag VSCODE-616 (#826) --- src/connectionController.ts | 27 ++++++----- src/participant/markdown.ts | 3 +- src/participant/participant.ts | 2 +- .../suite/participant/participant.test.ts | 47 +++++++++---------- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/connectionController.ts b/src/connectionController.ts index 4905ab8a6..0ca1a6b20 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -79,7 +79,12 @@ function isOIDCAuth(connectionString: string): boolean { // Exported for testing. export function getNotifyDeviceFlowForConnectionAttempt( connectionOptions: ConnectionOptions -) { +): + | ((deviceFlowInformation: { + verificationUrl: string; + userCode: string; + }) => void) + | undefined { const isOIDCConnectionAttempt = isOIDCAuth( connectionOptions.connectionString ); @@ -97,7 +102,7 @@ export function getNotifyDeviceFlowForConnectionAttempt( }: { verificationUrl: string; userCode: string; - }) => { + }): void => { void vscode.window.showInformationMessage( `Visit the following URL to complete authentication: ${verificationUrl} Enter the following code on that page: ${userCode}` ); @@ -381,7 +386,7 @@ export default class ConnectionController { ...cloneDeep(connectionOptions.oidc), openBrowser: browserAuthCommand ? { command: browserAuthCommand } - : async ({ signal, url }) => { + : async ({ signal, url }): Promise => { try { await openLink(url); } catch (err) { @@ -469,7 +474,7 @@ export default class ConnectionController { } // Used to re-authenticate with OIDC. - async _reauthenticationHandler() { + async _reauthenticationHandler(): Promise { const removeConfirmationResponse = await vscode.window.showInformationMessage( 'You need to re-authenticate to the database in order to continue.', @@ -488,7 +493,7 @@ export default class ConnectionController { }: { connectionInfo: LoadedConnection; dataService: DataService; - }) { + }): Promise { if (connectionInfo.storageLocation === 'NONE') { return; } @@ -513,7 +518,7 @@ export default class ConnectionController { // ?. because mocks in tests don't provide it dataService.on?.('connectionInfoSecretsChanged', () => { - void (async () => { + void (async (): Promise => { try { if ( !vscode.workspace.getConfiguration('mdb').get('persistOIDCTokens') @@ -546,7 +551,7 @@ export default class ConnectionController { }); } - cancelConnectionAttempt() { + cancelConnectionAttempt(): void { this._connectionAttempt?.cancelConnectionAttempt(); } @@ -806,11 +811,11 @@ export default class ConnectionController { this.eventEmitter.removeListener(eventType, listener); } - deactivate() { + deactivate(): void { this.eventEmitter.removeAllListeners(); } - closeConnectionStringInput() { + closeConnectionStringInput(): void { this._connectionStringInputCancellationToken?.cancel(); } @@ -905,7 +910,7 @@ export default class ConnectionController { return connectionStringData.toString(); } - isConnectedToAtlasStreams() { + isConnectedToAtlasStreams(): boolean { return ( this.isCurrentlyConnected() && isAtlasStream(this.getActiveConnectionString()) @@ -927,7 +932,7 @@ export default class ConnectionController { return connectionString; } - getActiveDataService() { + getActiveDataService(): DataService | null { return this._activeDataService; } diff --git a/src/participant/markdown.ts b/src/participant/markdown.ts index 6684a0c9f..73af90b85 100644 --- a/src/participant/markdown.ts +++ b/src/participant/markdown.ts @@ -16,9 +16,8 @@ export function createMarkdownLink({ const encodedData = encodeURIComponent(JSON.stringify(data)); const commandQueryString = data ? `?${encodedData}` : ''; const link = new vscode.MarkdownString( - `- ${name}\n` + `- [${name}](command:${commandId}${commandQueryString})\n` ); - link.supportHtml = true; link.isTrusted = { enabledCommands: [commandId] }; return link; } diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 4098014e9..c662cefdc 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1134,7 +1134,7 @@ export default class ParticipantController { if (docsResult.responseReferences) { for (const ref of docsResult.responseReferences) { const link = new vscode.MarkdownString( - `- ${ref.title}\n` + `- [${ref.title}](${ref.url})\n` ); link.supportHtml = true; stream.markdown(link); diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 53172b106..79f146bda 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -217,13 +217,13 @@ suite('Participant Controller Test Suite', function () { const listConnectionsMessage = chatStreamStub.markdown.getCall(1).args[0]; const expectedContent = encodeStringify({ id: 'id', command: '/query' }); expect(listConnectionsMessage.value).to.include( - `- localhost` + `- [localhost](command:mdb.connectWithParticipant?${expectedContent})` ); const showMoreMessage = chatStreamStub.markdown.getCall(2).args[0]; expect(showMoreMessage.value).to.include( - `- Show more` + })})` ); expect(chatResult?.metadata?.chatId.length).to.equal(testChatId.length); expect({ @@ -258,13 +258,13 @@ suite('Participant Controller Test Suite', function () { const listConnectionsMessage = chatStreamStub.markdown.getCall(1).args[0]; const expectedContent = encodeStringify({ id: 'id0', command: '/query' }); expect(listConnectionsMessage.value).to.include( - `- localhost0` + `- [localhost0](command:mdb.connectWithParticipant?${expectedContent})` ); const showMoreMessage = chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreMessage.value).to.include( - `- Show more` + })})` ); expect(chatStreamStub.markdown.callCount).to.be.eql(12); expect(chatResult?.metadata?.chatId.length).to.equal(testChatId.length); @@ -296,13 +296,13 @@ suite('Participant Controller Test Suite', function () { const listConnectionsMessage = chatStreamStub.markdown.getCall(4).args[0]; const expectedContent = encodeStringify({ id: 'id', command: '/query' }); expect(listConnectionsMessage.value).to.include( - `- localhost` + `- [localhost](command:mdb.connectWithParticipant?${expectedContent})` ); const showMoreMessage = chatStreamStub.markdown.getCall(5).args[0]; expect(showMoreMessage.value).to.include( - `- Show more` + })})` ); expect(chatResult?.metadata?.chatId.length).to.equal(testChatId.length); expect({ @@ -738,16 +738,15 @@ suite('Participant Controller Test Suite', function () { databaseName: 'dbOne', }); expect(listDBsMessage.value).to.include( - `- dbOne` + `- [dbOne](command:mdb.selectDatabaseWithParticipant?${expectedContent})` ); const showMoreDBsMessage = chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreDBsMessage.value).to.include( - `- Show more` + )})` ); - expect(showMoreDBsMessage.value).to.include('"'); expect(chatStreamStub.markdown.callCount).to.be.eql(12); const firstChatId = chatResult?.metadata?.chatId; expect(chatResult?.metadata?.chatId.length).to.equal( @@ -816,18 +815,18 @@ suite('Participant Controller Test Suite', function () { collectionName: 'collOne', }); expect(listCollsMessage.value).to.include( - `- collOne` + `- [collOne](command:mdb.selectCollectionWithParticipant?${expectedCollsContent})` ); const showMoreCollsMessage = chatStreamStub.markdown.getCall(23).args[0]; expect(showMoreCollsMessage.value).to.include( - `- Show more` + )})` ); expect(chatStreamStub.markdown.callCount).to.be.eql(24); expect(chatResult2?.metadata?.chatId).to.equal(firstChatId); @@ -974,23 +973,23 @@ suite('Participant Controller Test Suite', function () { ); const listDBsMessage = chatStreamStub.markdown.getCall(1).args[0]; expect(listDBsMessage.value).to.include( - `- dbOne` + )})` ); const showMoreDBsMessage = chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreDBsMessage.value).to.include( - `- Show more` + )})` ); expect({ ...chatResult?.metadata, @@ -1076,25 +1075,25 @@ suite('Participant Controller Test Suite', function () { ); const listCollsMessage = chatStreamStub.markdown.getCall(1).args[0]; expect(listCollsMessage.value).to.include( - `- collOne` + )})` ); const showMoreCollsMessage = chatStreamStub.markdown.getCall(11).args[0]; expect(showMoreCollsMessage.value).to.include( - `- Show more` + )})` ); expect({ ...chatResult?.metadata, From 5e0a09d1e4139c230d7c026ac3a29e99a6416a77 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 23 Sep 2024 21:04:02 +0200 Subject: [PATCH 30/45] feat(chat): Report docs fallback telemetry (#825) --- src/participant/participant.ts | 8 +++++++ src/telemetry/telemetryService.ts | 3 ++- .../suite/participant/participant.test.ts | 21 +++++++++++-------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index c662cefdc..09cc3707c 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1120,6 +1120,14 @@ export default class ParticipantController { // If the docs chatbot API is not available, fall back to Copilot’s LLM and include // the MongoDB documentation link for users to go to our documentation site directly. log.error(error); + this._telemetryService.track( + TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED, + { + command: 'docs', + error_name: ParticipantErrorTypes.DOCS_CHATBOT_API, + } + ); + docsResult = await this._handleDocsRequestWithCopilot(...args); } diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index a9608c8d7..53dd2cba5 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -105,7 +105,7 @@ type ParticipantFeedbackProperties = { type ParticipantResponseFailedProperties = { command: string; error_code?: string; - error_name: string; + error_name: ParticipantErrorTypes; }; export function chatResultFeedbackKindToTelemetryValue( @@ -167,6 +167,7 @@ export enum ParticipantErrorTypes { INVALID_PROMPT = 'Invalid Prompt', FILTERED = 'Filtered by Responsible AI Service', OTHER = 'Other', + DOCS_CHATBOT_API = 'Docs Chatbot API Issue', } /** diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 79f146bda..c415cffc6 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -64,7 +64,7 @@ suite('Participant Controller Test Suite', function () { }; let chatTokenStub; let countTokensStub; - let sendRequestStub; + let sendRequestStub: sinon.SinonStub; let telemetryTrackStub: SinonSpy; const invokeChatHandler = async ( @@ -399,13 +399,6 @@ suite('Participant Controller Test Suite', function () { TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN ); expect(telemetryTrackStub.lastCall.args[1]).to.be.undefined; - - telemetryTrackStub - .getCalls() - .map((call) => call.args[0]) - .filter( - (arg) => arg === TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN - ).length; }); }); @@ -1225,7 +1218,7 @@ Schema: suite('docs command', function () { const initialFetch = global.fetch; - let fetchStub; + let fetchStub: sinon.SinonStub; beforeEach(function () { sendRequestStub.onCall(0).resolves({ @@ -1274,6 +1267,16 @@ Schema: }; await invokeChatHandler(chatRequestMock); expect(sendRequestStub).to.have.been.called; + + // Expect the error to be reported through the telemetry service + sinon.assert.calledOnce(telemetryTrackStub); + expect(telemetryTrackStub.lastCall.args[0]).to.equal( + TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED + ); + + const properties = telemetryTrackStub.lastCall.args[1]; + expect(properties.command).to.equal('docs'); + expect(properties.error_name).to.equal('Docs Chatbot API Issue'); }); }); }); From 1b714a96a010cdd8fd56989e8dd42e1ebc91a071 Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 24 Sep 2024 08:36:09 -0400 Subject: [PATCH 31/45] chore(participant): add progress indicators for asynchronous requests in chat VSCODE-613 (#827) --- src/participant/participant.ts | 173 ++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 77 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 09cc3707c..306321063 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -449,24 +449,30 @@ export default class ParticipantController { ) as Promise; } - async getDatabasesTree({ + async renderDatabasesTree({ command, context, + stream, }: { command: ParticipantCommand; context: vscode.ChatContext; - }): Promise { + stream: vscode.ChatResponseStream; + }): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { - return []; + return; } + stream.push( + new vscode.ChatResponseProgressPart('Fetching database names...') + ); + try { const databases = await dataService.listDatabases({ nameOnly: true, }); - return [ - ...databases.slice(0, MAX_MARKDOWN_LIST_LENGTH).map((db) => + databases.slice(0, MAX_MARKDOWN_LIST_LENGTH).forEach((db) => + stream.markdown( createMarkdownLink({ commandId: EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, data: { @@ -478,46 +484,54 @@ export default class ParticipantController { }, name: db.name, }) - ), - ...(databases.length > MAX_MARKDOWN_LIST_LENGTH - ? [ - createMarkdownLink({ - data: { - command, - chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( - context.history - ), - }, - commandId: EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, - name: 'Show more', - }), - ] - : []), - ]; + ) + ); + if (databases.length > MAX_MARKDOWN_LIST_LENGTH) { + stream.markdown( + createMarkdownLink({ + data: { + command, + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ), + }, + commandId: EXTENSION_COMMANDS.SELECT_DATABASE_WITH_PARTICIPANT, + name: 'Show more', + }) + ); + } } catch (error) { + log.error('Unable to fetch databases:', error); + // Users can always do this manually when asked to provide a database name. - return []; + return; } } - async getCollectionTree({ + async renderCollectionsTree({ command, context, databaseName, + stream, }: { command: ParticipantCommand; databaseName: string; context: vscode.ChatContext; - }): Promise { + stream: vscode.ChatResponseStream; + }): Promise { const dataService = this._connectionController.getActiveDataService(); if (!dataService) { - return []; + return; } + stream.push( + new vscode.ChatResponseProgressPart('Fetching collection names...') + ); + try { const collections = await dataService.listCollections(databaseName); - return [ - ...collections.slice(0, MAX_MARKDOWN_LIST_LENGTH).map((coll) => + collections.slice(0, MAX_MARKDOWN_LIST_LENGTH).forEach((coll) => + stream.markdown( createMarkdownLink({ commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, data: { @@ -530,27 +544,28 @@ export default class ParticipantController { }, name: coll.name, }) - ), - ...(collections.length > MAX_MARKDOWN_LIST_LENGTH - ? [ - createMarkdownLink({ - commandId: - EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, - data: { - command, - chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( - context.history - ), - databaseName, - }, - name: 'Show more', - }), - ] - : []), - ]; + ) + ); + if (collections.length > MAX_MARKDOWN_LIST_LENGTH) { + stream.markdown( + createMarkdownLink({ + commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT, + data: { + command, + chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId( + context.history + ), + databaseName, + }, + name: 'Show more', + }) + ); + } } catch (error) { + log.error('Unable to fetch collections:', error); + // Users can always do this manually when asked to provide a collection name. - return []; + return; } } @@ -614,30 +629,26 @@ export default class ParticipantController { // we retrieve the available namespaces from the current connection. // Users can then select a value by clicking on an item in the list. if (!databaseName) { - const tree = await this.getDatabasesTree({ - command, - context, - }); stream.markdown( `What is the name of the database you would like${ command === '/query' ? ' this query' : '' } to run against?\n\n` ); - for (const item of tree) { - stream.markdown(item); - } - } else if (!collectionName) { - const tree = await this.getCollectionTree({ + await this.renderDatabasesTree({ command, - databaseName, context, + stream, }); + } else if (!collectionName) { stream.markdown( `Which collection would you like to use within ${databaseName}?\n\n` ); - for (const item of tree) { - stream.markdown(item); - } + await this.renderCollectionsTree({ + command, + databaseName, + context, + stream, + }); } return namespaceRequestChatResult({ @@ -675,12 +686,14 @@ export default class ParticipantController { collectionName, amountOfDocumentsToSample = NUM_DOCUMENTS_TO_SAMPLE, schemaFormat = 'simplified', + stream, }: { abortSignal; databaseName: string; collectionName: string; amountOfDocumentsToSample?: number; schemaFormat?: 'simplified' | 'full'; + stream: vscode.ChatResponseStream; }): Promise<{ schema?: string; sampleDocuments?: Document[]; @@ -693,6 +706,12 @@ export default class ParticipantController { }; } + stream.push( + new vscode.ChatResponseProgressPart( + 'Fetching documents and analyzing schema...' + ) + ); + try { const sampleDocuments = await dataService.sample( `${databaseName}.${collectionName}`, @@ -735,7 +754,7 @@ export default class ParticipantController { amountOfDocumentsSampled: sampleDocuments.length, }; } catch (err: any) { - log.error('Unable to fetch schema and sample documents', err); + log.error('Unable to fetch schema and sample documents:', err); throw err; } } @@ -759,7 +778,6 @@ export default class ParticipantController { // When the last message was asking for a database or collection name, // we re-ask the question. - let tree: vscode.MarkdownString[]; const databaseName = lastMessage.metadata.databaseName; if (databaseName) { stream.markdown( @@ -767,10 +785,11 @@ export default class ParticipantController { 'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.' ) ); - tree = await this.getCollectionTree({ + await this.renderCollectionsTree({ command, databaseName, context, + stream, }); } else { stream.markdown( @@ -778,16 +797,13 @@ export default class ParticipantController { 'Please select a database by either clicking on an item in the list or typing the name manually in the chat.' ) ); - tree = await this.getDatabasesTree({ + await this.renderDatabasesTree({ command, context, + stream, }); } - for (const item of tree) { - stream.markdown(item); - } - return namespaceRequestChatResult({ databaseName, collectionName: undefined, @@ -841,12 +857,6 @@ export default class ParticipantController { abortController.abort(); }); - stream.push( - new vscode.ChatResponseProgressPart( - 'Fetching documents and analyzing schema...' - ) - ); - let sampleDocuments: Document[] | undefined; let amountOfDocumentsSampled: number; let schema: string | undefined; @@ -861,6 +871,7 @@ export default class ParticipantController { schemaFormat: 'full', collectionName, amountOfDocumentsToSample: DOCUMENTS_TO_SAMPLE_FOR_SCHEMA_PROMPT, + stream, })); if (!schema || amountOfDocumentsSampled === 0) { @@ -971,6 +982,7 @@ export default class ParticipantController { abortSignal: abortController.signal, databaseName, collectionName, + stream, })); } catch (e) { // When an error fetching the collection schema or sample docs occurs, @@ -978,7 +990,7 @@ export default class ParticipantController { // we do want to notify the user. stream.markdown( vscode.l10n.t( - 'An error occurred while fetching the collection schema and sample documents.\nThe generated query will not be able to reference your data.' + 'An error occurred while fetching the collection schema and sample documents.\nThe generated query will not be able to reference the shape of your data.' ) ); } @@ -1012,14 +1024,20 @@ export default class ParticipantController { async _handleDocsRequestWithChatbot({ prompt, chatId, + stream, }: { prompt: string; chatId: string; + stream: vscode.ChatResponseStream; }): Promise<{ responseContent: string; responseReferences?: Reference[]; docsChatbotMessageId: string; }> { + stream.push( + new vscode.ChatResponseProgressPart('Consulting MongoDB documentation...') + ); + let { docsChatbotConversationId } = this._chatMetadataStore.getChatMetadata(chatId) ?? {}; if (!docsChatbotConversationId) { @@ -1115,6 +1133,7 @@ export default class ParticipantController { docsResult = await this._handleDocsRequestWithChatbot({ prompt: request.prompt, chatId, + stream, }); } catch (error) { // If the docs chatbot API is not available, fall back to Copilot’s LLM and include @@ -1171,9 +1190,9 @@ export default class ParticipantController { if (!hasBeenShownWelcomeMessageAlready) { stream.markdown( vscode.l10n.t(` - Welcome to MongoDB Participant!\n\n - Interact with your MongoDB clusters and generate MongoDB-related code more efficiently with intelligent AI-powered feature, available today in the MongoDB extension.\n\n - Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more information.\n\n`) +Welcome to MongoDB Participant!\n\n +Interact with your MongoDB clusters and generate MongoDB-related code more efficiently with intelligent AI-powered feature, available today in the MongoDB extension.\n\n +Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more information.\n\n`) ); this._telemetryService.track( From c0fe7a484cbbfb11fe38fbfedc78e06544b1073f Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 24 Sep 2024 08:42:46 -0400 Subject: [PATCH 32/45] chore(participant): add maxTimeMS to sample (#829) --- src/participant/participant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 306321063..aa798afc3 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -719,7 +719,7 @@ export default class ParticipantController { query: {}, size: amountOfDocumentsToSample, }, - { promoteValues: false }, + { promoteValues: false, maxTimeMS: 10_000 }, { abortSignal, } From 65830faa778c71f7258ec3c2b04520b84965adf1 Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 24 Sep 2024 09:08:51 -0400 Subject: [PATCH 33/45] chore(participant): add handling for token cancellation, pass to docs fetch request, remove unused abort controllers (#831) --- src/participant/constants.ts | 7 ++ src/participant/docsChatbotAIService.ts | 15 +++- src/participant/participant.ts | 81 ++++++++++++------- .../participant/docsChatbotAIService.test.ts | 30 ++++++- 4 files changed, 100 insertions(+), 33 deletions(-) diff --git a/src/participant/constants.ts b/src/participant/constants.ts index b03e66274..49e283d05 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -10,6 +10,7 @@ export type ParticipantResponseType = | 'docs' | 'generic' | 'emptyRequest' + | 'cancelledRequest' | 'askToConnect' | 'askForNamespace'; @@ -49,6 +50,12 @@ export function namespaceRequestChatResult({ }; } +export function createCancelledRequestChatResult( + history: ReadonlyArray +): ChatResult { + return createChatResult('cancelledRequest', history); +} + function createChatResult( intent: ParticipantResponseType, history: ReadonlyArray diff --git a/src/participant/docsChatbotAIService.ts b/src/participant/docsChatbotAIService.ts index 4b0c8b1b8..47858280f 100644 --- a/src/participant/docsChatbotAIService.ts +++ b/src/participant/docsChatbotAIService.ts @@ -54,14 +54,16 @@ export class DocsChatbotAIService { return `${this._serverBaseUri}api/${MONGODB_DOCS_CHATBOT_API_VERSION}${path}`; } - async _fetch({ + _fetch({ uri, method, body, + signal, headers, }: { uri: string; method: string; + signal?: AbortSignal; body?: string; headers?: { [key: string]: string }; }): Promise { @@ -72,15 +74,21 @@ export class DocsChatbotAIService { ...headers, }, method, + signal, ...(body && { body }), }); } - async createConversation(): Promise { + async createConversation({ + signal, + }: { + signal: AbortSignal; + }): Promise { const uri = this.getUri('/conversations'); const res = await this._fetch({ uri, method: 'POST', + signal, }); let data; @@ -113,9 +121,11 @@ export class DocsChatbotAIService { async addMessage({ conversationId, message, + signal, }: { conversationId: string; message: string; + signal: AbortSignal; }): Promise { const uri = this.getUri(`/conversations/${conversationId}/messages`); const res = await this._fetch({ @@ -123,6 +133,7 @@ export class DocsChatbotAIService { method: 'POST', body: JSON.stringify({ message }), headers: { 'Content-Type': 'application/json' }, + signal, }); let data; diff --git a/src/participant/participant.ts b/src/participant/participant.ts index aa798afc3..0e5920b70 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -20,6 +20,7 @@ import { queryRequestChatResult, docsRequestChatResult, schemaRequestChatResult, + createCancelledRequestChatResult, } from './constants'; import { QueryPrompt } from './prompts/query'; import { COL_NAME_ID, DB_NAME_ID, NamespacePrompt } from './prompts/namespace'; @@ -241,10 +242,6 @@ export default class ParticipantController { context, }); - const abortController = new AbortController(); - token.onCancellationRequested(() => { - abortController.abort(); - }); const responseContent = await this.getChatResponseContent({ messages, token, @@ -678,21 +675,33 @@ export default class ParticipantController { return askToConnectChatResult(context.history); } + _handleCancelledRequest({ + context, + stream, + }: { + context: vscode.ChatContext; + stream: vscode.ChatResponseStream; + }): ChatResult { + stream.markdown('\nRequest cancelled.'); + + return createCancelledRequestChatResult(context.history); + } + // The sample documents returned from this are simplified (strings and arrays shortened). // The sample documents are only returned when a user has the setting enabled. async _fetchCollectionSchemaAndSampleDocuments({ - abortSignal, databaseName, collectionName, amountOfDocumentsToSample = NUM_DOCUMENTS_TO_SAMPLE, schemaFormat = 'simplified', + token, stream, }: { - abortSignal; databaseName: string; collectionName: string; amountOfDocumentsToSample?: number; schemaFormat?: 'simplified' | 'full'; + token: vscode.CancellationToken; stream: vscode.ChatResponseStream; }): Promise<{ schema?: string; @@ -712,6 +721,11 @@ export default class ParticipantController { ) ); + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); + try { const sampleDocuments = await dataService.sample( `${databaseName}.${collectionName}`, @@ -721,7 +735,7 @@ export default class ParticipantController { }, { promoteValues: false, maxTimeMS: 10_000 }, { - abortSignal, + abortSignal: abortController.signal, } ); @@ -852,10 +866,12 @@ export default class ParticipantController { }); } - const abortController = new AbortController(); - token.onCancellationRequested(() => { - abortController.abort(); - }); + if (token.isCancellationRequested) { + return this._handleCancelledRequest({ + context, + stream, + }); + } let sampleDocuments: Document[] | undefined; let amountOfDocumentsSampled: number; @@ -866,11 +882,11 @@ export default class ParticipantController { amountOfDocumentsSampled, // There can be fewer than the amount we attempt to sample. schema, } = await this._fetchCollectionSchemaAndSampleDocuments({ - abortSignal: abortController.signal, databaseName, schemaFormat: 'full', collectionName, amountOfDocumentsToSample: DOCUMENTS_TO_SAMPLE_FOR_SCHEMA_PROMPT, + token, stream, })); @@ -969,19 +985,21 @@ export default class ParticipantController { }); } - const abortController = new AbortController(); - token.onCancellationRequested(() => { - abortController.abort(); - }); + if (token.isCancellationRequested) { + return this._handleCancelledRequest({ + context, + stream, + }); + } let schema: string | undefined; let sampleDocuments: Document[] | undefined; try { ({ schema, sampleDocuments } = await this._fetchCollectionSchemaAndSampleDocuments({ - abortSignal: abortController.signal, databaseName, collectionName, + token, stream, })); } catch (e) { @@ -1024,10 +1042,12 @@ export default class ParticipantController { async _handleDocsRequestWithChatbot({ prompt, chatId, + token, stream, }: { prompt: string; chatId: string; + token: vscode.CancellationToken; stream: vscode.ChatResponseStream; }): Promise<{ responseContent: string; @@ -1040,9 +1060,14 @@ export default class ParticipantController { let { docsChatbotConversationId } = this._chatMetadataStore.getChatMetadata(chatId) ?? {}; + const abortController = new AbortController(); + token.onCancellationRequested(() => { + abortController.abort(); + }); if (!docsChatbotConversationId) { - const conversation = - await this._docsChatbotAIService.createConversation(); + const conversation = await this._docsChatbotAIService.createConversation({ + signal: abortController.signal, + }); docsChatbotConversationId = conversation._id; this._chatMetadataStore.setChatMetadata(chatId, { docsChatbotConversationId, @@ -1053,6 +1078,7 @@ export default class ParticipantController { const response = await this._docsChatbotAIService.addMessage({ message: prompt, conversationId: docsChatbotConversationId, + signal: abortController.signal, }); log.info('Docs chatbot message sent', { @@ -1085,10 +1111,6 @@ export default class ParticipantController { context, }); - const abortController = new AbortController(); - token.onCancellationRequested(() => { - abortController.abort(); - }); const responseContent = await this.getChatResponseContent({ messages, token, @@ -1115,10 +1137,6 @@ export default class ParticipantController { ] ): Promise { const [request, context, stream, token] = args; - const abortController = new AbortController(); - token.onCancellationRequested(() => { - abortController.abort(); - }); const chatId = ChatMetadataStore.getChatIdFromHistoryOrNewChatId( context.history @@ -1133,12 +1151,21 @@ export default class ParticipantController { docsResult = await this._handleDocsRequestWithChatbot({ prompt: request.prompt, chatId, + token, stream, }); } catch (error) { // If the docs chatbot API is not available, fall back to Copilot’s LLM and include // the MongoDB documentation link for users to go to our documentation site directly. log.error(error); + + if (token.isCancellationRequested) { + return this._handleCancelledRequest({ + context, + stream, + }); + } + this._telemetryService.track( TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED, { diff --git a/src/test/suite/participant/docsChatbotAIService.test.ts b/src/test/suite/participant/docsChatbotAIService.test.ts index 827cf6841..e0f97e7ff 100644 --- a/src/test/suite/participant/docsChatbotAIService.test.ts +++ b/src/test/suite/participant/docsChatbotAIService.test.ts @@ -28,7 +28,9 @@ suite('DocsChatbotAIService Test Suite', function () { }), }); global.fetch = fetchStub; - const conversation = await docsChatbotAIService.createConversation(); + const conversation = await docsChatbotAIService.createConversation({ + signal: new AbortController().signal, + }); expect(conversation._id).to.be.eql('650b4b260f975ef031016c8a'); }); @@ -42,13 +44,28 @@ suite('DocsChatbotAIService Test Suite', function () { global.fetch = fetchStub; try { - await docsChatbotAIService.createConversation(); + await docsChatbotAIService.createConversation({ + signal: new AbortController().signal, + }); expect.fail('It must fail with the server error'); } catch (error) { expect((error as Error).message).to.include('Internal server error'); } }); + test('throws when aborted', async () => { + try { + const abortController = new AbortController(); + abortController.abort(); + await docsChatbotAIService.createConversation({ + signal: abortController.signal, + }); + expect.fail('It must fail with the server error'); + } catch (error) { + expect((error as Error).message).to.include('This operation was aborted'); + } + }); + test('throws on bad requests', async () => { const fetchStub = sinon.stub().resolves({ status: 400, @@ -59,7 +76,9 @@ suite('DocsChatbotAIService Test Suite', function () { global.fetch = fetchStub; try { - await docsChatbotAIService.createConversation(); + await docsChatbotAIService.createConversation({ + signal: new AbortController().signal, + }); expect.fail('It must fail with the bad request error'); } catch (error) { expect((error as Error).message).to.include('Bad request'); @@ -76,7 +95,9 @@ suite('DocsChatbotAIService Test Suite', function () { global.fetch = fetchStub; try { - await docsChatbotAIService.createConversation(); + await docsChatbotAIService.createConversation({ + signal: new AbortController().signal, + }); expect.fail('It must fail with the rate limited error'); } catch (error) { expect((error as Error).message).to.include('Rate limited'); @@ -95,6 +116,7 @@ suite('DocsChatbotAIService Test Suite', function () { await docsChatbotAIService.addMessage({ conversationId: '650b4b260f975ef031016c8a', message: 'what is mongosh?', + signal: new AbortController().signal, }); expect.fail('It must fail with the timeout error'); } catch (error) { From 89c94e3f5d312b83300a50ab8e3ac7ca2e8c6317 Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 24 Sep 2024 12:28:41 -0400 Subject: [PATCH 34/45] chore(participant): update query prompt, update accuracy test config to include proposed api types (#828) --- package.json | 2 +- src/participant/prompts/query.ts | 86 ++++++++++--------- .../ai-accuracy-tests/ai-accuracy-tests.ts | 4 +- .../suite/participant/participant.test.ts | 12 +-- 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index d1d102828..65c6f1625 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "test": "npm run test-webview && npm run test-extension", "test-extension": "cross-env NODE_OPTIONS=--no-force-async-hooks-checks xvfb-maybe node ./out/test/runTest.js", "test-webview": "mocha -r ts-node/register --file ./src/test/setup-webview.ts src/test/suite/views/webview-app/**/*.test.tsx", - "ai-accuracy-tests": "mocha -r ts-node/register --file ./src/test/ai-accuracy-tests/test-setup.ts ./src/test/ai-accuracy-tests/ai-accuracy-tests.ts", + "ai-accuracy-tests": "env TS_NODE_FILES=true mocha -r ts-node/register --file ./src/test/ai-accuracy-tests/test-setup.ts ./src/test/ai-accuracy-tests/ai-accuracy-tests.ts", "analyze-bundle": "webpack --mode production --analyze", "vscode:prepublish": "npm run clean && npm run compile:constants && npm run compile:resources && webpack --mode production", "check": "npm run lint && npm run depcheck", diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index 9a1e3ebeb..123ceb560 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -5,56 +5,62 @@ import { getHistoryMessages } from './history'; import { getStringifiedSampleDocuments } from '../sampleDocuments'; export class QueryPrompt { - static async getAssistantPrompt({ - databaseName = 'mongodbVSCodeCopilotDB', - collectionName = 'test', - schema, - sampleDocuments, - }: { - databaseName?: string; - collectionName?: string; - schema?: string; - sampleDocuments?: Document[]; - }): Promise { - let prompt = `You are a MongoDB expert. - -Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. + static getAssistantPrompt(): vscode.LanguageModelChatMessage { + const prompt = `You are a MongoDB expert. +Your task is to help the user craft MongoDB shell syntax code to perform their task. Keep your response concise. -You should suggest queries that are performant and correct. -Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. -You can imagine the schema. +You must suggest code that is performant and correct. +Respond with markdown, write code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax. -You can use only the following MongoDB Shell commands: use, aggregate, bulkWrite, countDocuments, findOneAndReplace, -findOneAndUpdate, insert, insertMany, insertOne, remove, replaceOne, update, updateMany, updateOne. Concisely explain the code snippet you have generated. Example 1: -use(''); -db.getCollection('').aggregate([ +User: Documents in the orders db, sales collection, where the date is in 2014 and group the total sales for each product. +Response: +\`\`\`javascript +use('orders'); +db.getCollection('sales').aggregate([ // Find all of the sales that occurred in 2014. { $match: { date: { $gte: new Date('2014-01-01'), $lt: new Date('2015-01-01') } } }, // Group the total sales for each product. { $group: { _id: '$item', totalSaleAmount: { $sum: { $multiply: [ '$price', '$quantity' ] } } } } ]); +\`\`\` Example 2: -use(''); -db.getCollection('').find({ - date: { $gte: new Date('2014-04-04'), $lt: new Date('2014-04-05') } -}).count(); +User: How do I create an index on the name field in my users collection?. +Response: +\`\`\`javascript +use('test'); +db.getCollection('users').createIndex({ name: 1 }); +\`\`\` MongoDB command to specify database: use(''); MongoDB command to specify collection: -db.getCollection('');\n\n`; - if (databaseName) { - prompt += `Database name: ${databaseName}\n`; - } - if (collectionName) { - prompt += `Collection name: ${collectionName}\n`; - } +db.getCollection('');\n`; + + // eslint-disable-next-line new-cap + return vscode.LanguageModelChatMessage.Assistant(prompt); + } + + static async getUserPrompt({ + databaseName = 'mongodbVSCodeCopilotDB', + collectionName = 'test', + prompt, + schema, + sampleDocuments, + }: { + databaseName: string; + collectionName: string; + prompt: string; + schema?: string; + sampleDocuments?: Document[]; + }): Promise { + prompt += `\nDatabase name: ${databaseName}\n`; + prompt += `Collection name: ${collectionName}\n`; if (schema) { prompt += `Collection schema: ${schema}\n`; } @@ -65,11 +71,6 @@ db.getCollection('');\n\n`; }); } - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.Assistant(prompt); - } - - static getUserPrompt(prompt: string): vscode.LanguageModelChatMessage { // eslint-disable-next-line new-cap return vscode.LanguageModelChatMessage.User(prompt); } @@ -87,21 +88,22 @@ db.getCollection('');\n\n`; prompt: string; }; context: vscode.ChatContext; - databaseName?: string; - collectionName?: string; + databaseName: string; + collectionName: string; schema?: string; sampleDocuments?: Document[]; connectionNames: string[]; }): Promise { const messages = [ - await QueryPrompt.getAssistantPrompt({ + QueryPrompt.getAssistantPrompt(), + ...getHistoryMessages({ context, connectionNames }), + await QueryPrompt.getUserPrompt({ databaseName, collectionName, + prompt: request.prompt, schema, sampleDocuments, }), - ...getHistoryMessages({ context, connectionNames }), - QueryPrompt.getUserPrompt(request.prompt), ]; return messages; diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index 712a98647..660b0a169 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -320,8 +320,8 @@ const buildMessages = async ({ return await QueryPrompt.buildMessages({ request: { prompt: testCase.userInput }, context: { history: [] }, - databaseName: testCase.databaseName, - collectionName: testCase.collectionName, + databaseName: testCase.databaseName ?? 'test', + collectionName: testCase.collectionName ?? 'test', connectionNames: [], ...(fixtures[testCase.databaseName as string]?.[ testCase.collectionName as string diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index c415cffc6..6e1e9889e 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -501,7 +501,7 @@ suite('Participant Controller Test Suite', function () { }; await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; - expect(messages[0].content).to.include( + expect(messages[1].content).to.include( 'Collection schema: _id: ObjectId\n' + 'field.stringField: String\n' + 'field.arrayField: Array\n' @@ -550,7 +550,7 @@ suite('Participant Controller Test Suite', function () { }; await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; - expect(messages[0].content).to.include( + expect(messages[1].content).to.include( 'Sample documents: [\n' + ' {\n' + " _id: ObjectId('63ed1d522d8573fa5c203661'),\n" + @@ -603,7 +603,7 @@ suite('Participant Controller Test Suite', function () { }; await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; - expect(messages[0].content).to.include( + expect(messages[1].content).to.include( 'Sample document: {\n' + " _id: ObjectId('63ed1d522d8573fa5c203660'),\n" + ' field: {\n' + @@ -650,7 +650,7 @@ suite('Participant Controller Test Suite', function () { }; await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; - expect(messages[0].content).to.include( + expect(messages[1].content).to.include( 'Sample document: {\n' + " _id: ObjectId('63ed1d522d8573fa5c203661'),\n" + ' field: {\n' + @@ -694,7 +694,7 @@ suite('Participant Controller Test Suite', function () { }; await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; - expect(messages[0].content).to.not.include('Sample documents'); + expect(messages[1].content).to.not.include('Sample documents'); }); }); @@ -707,7 +707,7 @@ suite('Participant Controller Test Suite', function () { }; await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; - expect(messages[0].content).to.not.include('Sample documents'); + expect(messages[1].content).to.not.include('Sample documents'); }); }); }); From 08fac17ac6ca065917d11beeff26d4de55731fac Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 26 Sep 2024 02:00:38 +0200 Subject: [PATCH 35/45] chore(chat): Refactor prompt hierarchy (#834) * Refactor prompt hierarchy * reformat * Fix build * Address feedback, remove connect messages from all prompt builders * Add tests and fix historyMessages not removing the pre-connection prompt * Remove .only --- src/participant/constants.ts | 11 +- src/participant/participant.ts | 86 +++---- src/participant/prompts/generic.ts | 42 +--- src/participant/prompts/history.ts | 82 ------- src/participant/prompts/index.ts | 16 ++ src/participant/prompts/namespace.ts | 75 ++---- src/participant/prompts/promptBase.ts | 127 ++++++++++ src/participant/prompts/query.ts | 72 ++---- src/participant/prompts/schema.ts | 81 ++----- src/participant/sampleDocuments.ts | 16 +- .../ai-accuracy-tests/ai-accuracy-tests.ts | 32 ++- .../suite/participant/participant.test.ts | 219 +++++++++++++++++- 12 files changed, 501 insertions(+), 358 deletions(-) delete mode 100644 src/participant/prompts/history.ts create mode 100644 src/participant/prompts/index.ts create mode 100644 src/participant/prompts/promptBase.ts diff --git a/src/participant/constants.ts b/src/participant/constants.ts index 49e283d05..b43b1332f 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -15,9 +15,8 @@ export type ParticipantResponseType = | 'askForNamespace'; interface Metadata { - intent: Exclude; + intent: Exclude; chatId: string; - docsChatbotMessageId?: string; } interface AskForNamespaceMetadata { @@ -27,8 +26,14 @@ interface AskForNamespaceMetadata { collectionName?: string | undefined; } +interface DocsRequestMetadata { + intent: 'docs'; + chatId: string; + docsChatbotMessageId?: string; +} + export interface ChatResult extends vscode.ChatResult { - readonly metadata: Metadata | AskForNamespaceMetadata; + readonly metadata: Metadata | AskForNamespaceMetadata | DocsRequestMetadata; } export function namespaceRequestChatResult({ diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 0e5920b70..dc9e94a17 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -9,7 +9,7 @@ import type { LoadedConnection } from '../storage/connectionStorage'; import EXTENSION_COMMANDS from '../commands'; import type { StorageController } from '../storage'; import { StorageVariables } from '../storage'; -import { GenericPrompt, isPromptEmpty } from './prompts/generic'; +import { Prompts } from './prompts'; import type { ChatResult } from './constants'; import { askToConnectChatResult, @@ -22,18 +22,14 @@ import { schemaRequestChatResult, createCancelledRequestChatResult, } from './constants'; -import { QueryPrompt } from './prompts/query'; -import { COL_NAME_ID, DB_NAME_ID, NamespacePrompt } from './prompts/namespace'; import { SchemaFormatter } from './schema'; import { getSimplifiedSampleDocuments } from './sampleDocuments'; import { getCopilotModel } from './model'; import { createMarkdownLink } from './markdown'; import { ChatMetadataStore } from './chatMetadata'; -import { doesLastMessageAskForNamespace } from './prompts/history'; import { DOCUMENTS_TO_SAMPLE_FOR_SCHEMA_PROMPT, type OpenSchemaCommandArgs, - SchemaPrompt, } from './prompts/schema'; import { chatResultFeedbackKindToTelemetryValue, @@ -58,22 +54,10 @@ export type RunParticipantQueryCommandArgs = { runnableContent: string; }; -const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)`; -const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)`; - export type ParticipantCommand = '/query' | '/schema' | '/docs'; const MAX_MARKDOWN_LIST_LENGTH = 10; -export function parseForDatabaseAndCollectionName(text: string): { - databaseName?: string; - collectionName?: string; -} { - const databaseName = text.match(DB_NAME_REGEX)?.[1].trim(); - const collectionName = text.match(COL_NAME_REGEX)?.[1].trim(); - return { databaseName, collectionName }; -} - export function getRunnableContentFromString(text: string): string { const matchedJSresponseContent = text.match(/```javascript((.|\n)*)```/); @@ -237,9 +221,10 @@ export default class ParticipantController { stream: vscode.ChatResponseStream, token: vscode.CancellationToken ): Promise { - const messages = GenericPrompt.buildMessages({ + const messages = await Prompts.generic.buildMessages({ request, context, + connectionNames: this._getConnectionNames(), }); const responseContent = await this.getChatResponseContent({ @@ -578,20 +563,19 @@ export default class ParticipantController { databaseName: string | undefined; collectionName: string | undefined; }> { - const messagesWithNamespace = NamespacePrompt.buildMessages({ + const messagesWithNamespace = await Prompts.namespace.buildMessages({ context, request, - connectionNames: this._connectionController - .getSavedConnections() - .map((connection) => connection.name), + connectionNames: this._getConnectionNames(), }); const responseContentWithNamespace = await this.getChatResponseContent({ messages: messagesWithNamespace, token, }); - const namespace = parseForDatabaseAndCollectionName( - responseContentWithNamespace - ); + const { databaseName, collectionName } = + Prompts.namespace.extractDatabaseAndCollectionNameFromResponse( + responseContentWithNamespace + ); // See if there's a namespace set in the // chat metadata we can fallback to if the model didn't find it. @@ -604,8 +588,8 @@ export default class ParticipantController { } = this._chatMetadataStore.getChatMetadata(chatId) ?? {}; return { - databaseName: namespace.databaseName ?? databaseNameFromMetadata, - collectionName: namespace.collectionName ?? collectionNameFromMetadata, + databaseName: databaseName || databaseNameFromMetadata, + collectionName: collectionName || collectionNameFromMetadata, }; } @@ -655,6 +639,19 @@ export default class ParticipantController { }); } + _doesLastMessageAskForNamespace( + history: ReadonlyArray + ): boolean { + const lastMessageMetaData = history[ + history.length - 1 + ] as vscode.ChatResponseTurn; + + return ( + (lastMessageMetaData?.result as ChatResult)?.metadata?.intent === + 'askForNamespace' + ); + } + _askToConnect({ command, context, @@ -786,7 +783,7 @@ export default class ParticipantController { .history[context.history.length - 1] as vscode.ChatResponseTurn; const lastMessage = lastMessageMetaData?.result as ChatResult; if (lastMessage?.metadata?.intent !== 'askForNamespace') { - stream.markdown(GenericPrompt.getEmptyRequestResponse()); + stream.markdown(Prompts.generic.getEmptyRequestResponse()); return emptyRequestChatResult(context.history); } @@ -841,8 +838,8 @@ export default class ParticipantController { } if ( - isPromptEmpty(request) && - doesLastMessageAskForNamespace(context.history) + Prompts.isPromptEmpty(request) && + this._doesLastMessageAskForNamespace(context.history) ) { return this.handleEmptyNamespaceMessage({ command: '/schema', @@ -907,16 +904,14 @@ export default class ParticipantController { return schemaRequestChatResult(context.history); } - const messages = SchemaPrompt.buildMessages({ + const messages = await Prompts.schema.buildMessages({ request, context, databaseName, amountOfDocumentsSampled, collectionName, schema, - connectionNames: this._connectionController - .getSavedConnections() - .map((connection) => connection.name), + connectionNames: this._getConnectionNames(), ...(sampleDocuments ? { sampleDocuments } : {}), }); const responseContent = await this.getChatResponseContent({ @@ -953,8 +948,8 @@ export default class ParticipantController { }); } - if (isPromptEmpty(request)) { - if (doesLastMessageAskForNamespace(context.history)) { + if (Prompts.isPromptEmpty(request)) { + if (this._doesLastMessageAskForNamespace(context.history)) { return this.handleEmptyNamespaceMessage({ command: '/query', context, @@ -962,7 +957,7 @@ export default class ParticipantController { }); } - stream.markdown(QueryPrompt.getEmptyRequestResponse()); + stream.markdown(Prompts.query.emptyRequestResponse); return emptyRequestChatResult(context.history); } @@ -1013,15 +1008,13 @@ export default class ParticipantController { ); } - const messages = await QueryPrompt.buildMessages({ + const messages = await Prompts.query.buildMessages({ request, context, databaseName, collectionName, schema, - connectionNames: this._connectionController - .getSavedConnections() - .map((connection) => connection.name), + connectionNames: this._getConnectionNames(), ...(sampleDocuments ? { sampleDocuments } : {}), }); const responseContent = await this.getChatResponseContent({ @@ -1106,9 +1099,10 @@ export default class ParticipantController { responseReferences?: Reference[]; }> { const [request, context, , token] = args; - const messages = GenericPrompt.buildMessages({ + const messages = await Prompts.generic.buildMessages({ request, context, + connectionNames: this._getConnectionNames(), }); const responseContent = await this.getChatResponseContent({ @@ -1241,7 +1235,7 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i return await this.handleSchemaRequest(...args); default: if (!request.prompt?.trim()) { - stream.markdown(GenericPrompt.getEmptyRequestResponse()); + stream.markdown(Prompts.generic.getEmptyRequestResponse()); return emptyRequestChatResult(args[1].history); } @@ -1291,4 +1285,10 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i response_type: (feedback.result as ChatResult)?.metadata.intent, }); } + + _getConnectionNames(): string[] { + return this._connectionController + .getSavedConnections() + .map((connection) => connection.name); + } } diff --git a/src/participant/prompts/generic.ts b/src/participant/prompts/generic.ts index 733a86fe9..9b5348790 100644 --- a/src/participant/prompts/generic.ts +++ b/src/participant/prompts/generic.ts @@ -1,52 +1,22 @@ import * as vscode from 'vscode'; +import type { PromptArgsBase } from './promptBase'; +import { PromptBase } from './promptBase'; -import { getHistoryMessages } from './history'; - -export class GenericPrompt { - static getAssistantPrompt(): vscode.LanguageModelChatMessage { - const prompt = `You are a MongoDB expert. +export class GenericPrompt extends PromptBase { + protected getAssistantPrompt(): string { + return `You are a MongoDB expert. Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. Keep your response concise. You should suggest queries that are performant and correct. Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. You can imagine the schema, collection, and database name. Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; - - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.Assistant(prompt); } - static getUserPrompt(prompt: string): vscode.LanguageModelChatMessage { - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.User(prompt); - } - - static getEmptyRequestResponse(): string { + public getEmptyRequestResponse(): string { // TODO(VSCODE-572): Generic empty response handler return vscode.l10n.t( 'Ask anything about MongoDB, from writing queries to questions about your cluster.' ); } - - static buildMessages({ - context, - request, - }: { - request: { - prompt: string; - }; - context: vscode.ChatContext; - }): vscode.LanguageModelChatMessage[] { - const messages = [ - GenericPrompt.getAssistantPrompt(), - ...getHistoryMessages({ context }), - GenericPrompt.getUserPrompt(request.prompt), - ]; - - return messages; - } -} - -export function isPromptEmpty(request: vscode.ChatRequest): boolean { - return !request.prompt || request.prompt.trim().length === 0; } diff --git a/src/participant/prompts/history.ts b/src/participant/prompts/history.ts deleted file mode 100644 index 4f3bfe25d..000000000 --- a/src/participant/prompts/history.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as vscode from 'vscode'; -import type { ChatResult, ParticipantResponseType } from '../constants'; - -// When passing the history to the model we only want contextual messages -// to be passed. This function parses through the history and returns -// the messages that are valuable to keep. -// eslint-disable-next-line complexity -export function getHistoryMessages({ - connectionNames, - context, -}: { - connectionNames?: string[]; // Used to scrape the connecting messages from the history. - context: vscode.ChatContext; -}): vscode.LanguageModelChatMessage[] { - const messages: vscode.LanguageModelChatMessage[] = []; - - for (const historyItem of context.history) { - if (historyItem instanceof vscode.ChatRequestTurn) { - if ( - historyItem.prompt?.trim().length === 0 || - connectionNames?.includes(historyItem.prompt) - ) { - // When the message is empty or a connection name then we skip it. - // It's probably going to be the response to the connect step. - continue; - } - - // eslint-disable-next-line new-cap - messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt)); - } - - if (historyItem instanceof vscode.ChatResponseTurn) { - let message = ''; - - // Skip a response to an empty user prompt message or connect message. - const responseTypesToSkip: ParticipantResponseType[] = [ - 'emptyRequest', - 'askToConnect', - ]; - if ( - responseTypesToSkip.indexOf( - (historyItem.result as ChatResult)?.metadata?.intent - ) > -1 - ) { - continue; - } - - for (const fragment of historyItem.response) { - if (fragment instanceof vscode.ChatResponseMarkdownPart) { - message += fragment.value.value; - - if ( - (historyItem.result as ChatResult)?.metadata?.intent === - 'askForNamespace' - ) { - // When the message is the assistant asking for part of a namespace, - // we only want to include the question asked, not the user's - // database and collection names in the history item. - break; - } - } - } - // eslint-disable-next-line new-cap - messages.push(vscode.LanguageModelChatMessage.Assistant(message)); - } - } - - return messages; -} - -export function doesLastMessageAskForNamespace( - history: ReadonlyArray -): boolean { - const lastMessageMetaData: vscode.ChatResponseTurn | undefined = history[ - history.length - 1 - ] as vscode.ChatResponseTurn; - - return ( - (lastMessageMetaData?.result as ChatResult)?.metadata?.intent === - 'askForNamespace' - ); -} diff --git a/src/participant/prompts/index.ts b/src/participant/prompts/index.ts new file mode 100644 index 000000000..2d40beec4 --- /dev/null +++ b/src/participant/prompts/index.ts @@ -0,0 +1,16 @@ +import { GenericPrompt } from './generic'; +import type * as vscode from 'vscode'; +import { NamespacePrompt } from './namespace'; +import { QueryPrompt } from './query'; +import { SchemaPrompt } from './schema'; + +export class Prompts { + public static generic = new GenericPrompt(); + public static namespace = new NamespacePrompt(); + public static query = new QueryPrompt(); + public static schema = new SchemaPrompt(); + + public static isPromptEmpty(request: vscode.ChatRequest): boolean { + return !request.prompt || request.prompt.trim().length === 0; + } +} diff --git a/src/participant/prompts/namespace.ts b/src/participant/prompts/namespace.ts index 787517aa2..e29f24d2c 100644 --- a/src/participant/prompts/namespace.ts +++ b/src/participant/prompts/namespace.ts @@ -1,14 +1,15 @@ -import * as vscode from 'vscode'; +import type { PromptArgsBase } from './promptBase'; +import { PromptBase } from './promptBase'; -import { getHistoryMessages } from './history'; -import type { ChatResult } from '../constants'; +const DB_NAME_ID = 'DATABASE_NAME'; +const COL_NAME_ID = 'COLLECTION_NAME'; -export const DB_NAME_ID = 'DATABASE_NAME'; -export const COL_NAME_ID = 'COLLECTION_NAME'; +const DB_NAME_REGEX = `${DB_NAME_ID}: (.*)`; +const COL_NAME_REGEX = `${COL_NAME_ID}: (.*)`; -export class NamespacePrompt { - static getAssistantPrompt(): vscode.LanguageModelChatMessage { - const prompt = `You are a MongoDB expert. +export class NamespacePrompt extends PromptBase { + protected getAssistantPrompt(): string { + return `You are a MongoDB expert. Parse all user messages to find a database name and a collection name. Respond in the format: ${DB_NAME_ID}: X @@ -39,58 +40,14 @@ User: Where is the best hummus in Berlin? Response: No names found. `; - - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.Assistant(prompt); - } - - static getUserPrompt(prompt: string): vscode.LanguageModelChatMessage { - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.User(prompt); } - static buildMessages({ - context, - request, - connectionNames, - }: { - request: { - prompt: string; - }; - context: vscode.ChatContext; - connectionNames: string[]; - }): vscode.LanguageModelChatMessage[] { - let historyMessages = getHistoryMessages({ context, connectionNames }); - // If the current user's prompt is a connection name, and the last - // message was to connect. We want to use the last - // message they sent before the connection name as their prompt. - let userPrompt = request.prompt; - if (connectionNames.includes(request.prompt)) { - const previousResponse = context.history[ - context.history.length - 1 - ] as vscode.ChatResponseTurn; - const intent = (previousResponse?.result as ChatResult).metadata.intent; - if (intent === 'askToConnect') { - // Go through the history in reverse order to find the last user message. - for (let i = historyMessages.length - 1; i >= 0; i--) { - if ( - historyMessages[i].role === vscode.LanguageModelChatMessageRole.User - ) { - userPrompt = historyMessages[i].content; - // Remove the item from the history messages array. - historyMessages = historyMessages.slice(0, i); - break; - } - } - } - } - - const messages = [ - NamespacePrompt.getAssistantPrompt(), - ...historyMessages, - NamespacePrompt.getUserPrompt(userPrompt), - ]; - - return messages; + extractDatabaseAndCollectionNameFromResponse(text: string): { + databaseName?: string; + collectionName?: string; + } { + const databaseName = text.match(DB_NAME_REGEX)?.[1].trim(); + const collectionName = text.match(COL_NAME_REGEX)?.[1].trim(); + return { databaseName, collectionName }; } } diff --git a/src/participant/prompts/promptBase.ts b/src/participant/prompts/promptBase.ts new file mode 100644 index 000000000..0f3c83286 --- /dev/null +++ b/src/participant/prompts/promptBase.ts @@ -0,0 +1,127 @@ +import * as vscode from 'vscode'; +import type { ChatResult, ParticipantResponseType } from '../constants'; + +export interface PromptArgsBase { + request: { + prompt: string; + command?: string; + }; + context: vscode.ChatContext; + connectionNames: string[]; +} + +export abstract class PromptBase { + protected abstract getAssistantPrompt(args: TArgs): string; + + protected getUserPrompt(args: TArgs): Promise { + return Promise.resolve(args.request.prompt); + } + + async buildMessages(args: TArgs): Promise { + let historyMessages = this.getHistoryMessages(args); + // If the current user's prompt is a connection name, and the last + // message was to connect. We want to use the last + // message they sent before the connection name as their prompt. + if (args.connectionNames.includes(args.request.prompt)) { + const history = args.context.history; + const previousResponse = history[ + history.length - 1 + ] as vscode.ChatResponseTurn; + const intent = (previousResponse?.result as ChatResult)?.metadata.intent; + if (intent === 'askToConnect') { + // Go through the history in reverse order to find the last user message. + for (let i = history.length - 1; i >= 0; i--) { + if (history[i] instanceof vscode.ChatRequestTurn) { + // Rewrite the arguments so that the prompt is the last user message from history + args = { + ...args, + request: { + ...args.request, + prompt: (history[i] as vscode.ChatRequestTurn).prompt, + }, + }; + + // Remove the item from the history messages array. + historyMessages = historyMessages.slice(0, i); + break; + } + } + } + } + + return [ + // eslint-disable-next-line new-cap + vscode.LanguageModelChatMessage.Assistant(this.getAssistantPrompt(args)), + ...historyMessages, + // eslint-disable-next-line new-cap + vscode.LanguageModelChatMessage.User(await this.getUserPrompt(args)), + ]; + } + + // When passing the history to the model we only want contextual messages + // to be passed. This function parses through the history and returns + // the messages that are valuable to keep. + // eslint-disable-next-line complexity + protected getHistoryMessages({ + connectionNames, + context, + }: { + connectionNames: string[]; // Used to scrape the connecting messages from the history. + context: vscode.ChatContext; + }): vscode.LanguageModelChatMessage[] { + const messages: vscode.LanguageModelChatMessage[] = []; + + for (const historyItem of context.history) { + if (historyItem instanceof vscode.ChatRequestTurn) { + if ( + historyItem.prompt?.trim().length === 0 || + connectionNames?.includes(historyItem.prompt) + ) { + // When the message is empty or a connection name then we skip it. + // It's probably going to be the response to the connect step. + continue; + } + + // eslint-disable-next-line new-cap + messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt)); + } + + if (historyItem instanceof vscode.ChatResponseTurn) { + let message = ''; + + // Skip a response to an empty user prompt message or connect message. + const responseTypesToSkip: ParticipantResponseType[] = [ + 'emptyRequest', + 'askToConnect', + ]; + if ( + responseTypesToSkip.indexOf( + (historyItem.result as ChatResult)?.metadata?.intent + ) > -1 + ) { + continue; + } + + for (const fragment of historyItem.response) { + if (fragment instanceof vscode.ChatResponseMarkdownPart) { + message += fragment.value.value; + + if ( + (historyItem.result as ChatResult)?.metadata?.intent === + 'askForNamespace' + ) { + // When the message is the assistant asking for part of a namespace, + // we only want to include the question asked, not the user's + // database and collection names in the history item. + break; + } + } + } + // eslint-disable-next-line new-cap + messages.push(vscode.LanguageModelChatMessage.Assistant(message)); + } + } + + return messages; + } +} diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index 123ceb560..b7ae5cc26 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -1,12 +1,21 @@ import * as vscode from 'vscode'; import type { Document } from 'bson'; -import { getHistoryMessages } from './history'; import { getStringifiedSampleDocuments } from '../sampleDocuments'; +import type { PromptArgsBase } from './promptBase'; +import { PromptBase } from './promptBase'; -export class QueryPrompt { - static getAssistantPrompt(): vscode.LanguageModelChatMessage { - const prompt = `You are a MongoDB expert. +interface QueryPromptArgs extends PromptArgsBase { + databaseName: string; + collectionName: string; + schema?: string; + sampleDocuments?: Document[]; + connectionNames: string[]; +} + +export class QueryPrompt extends PromptBase { + protected getAssistantPrompt(): string { + return `You are a MongoDB expert. Your task is to help the user craft MongoDB shell syntax code to perform their task. Keep your response concise. You must suggest code that is performant and correct. @@ -41,24 +50,16 @@ use(''); MongoDB command to specify collection: db.getCollection('');\n`; - - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.Assistant(prompt); } - static async getUserPrompt({ + async getUserPrompt({ databaseName = 'mongodbVSCodeCopilotDB', collectionName = 'test', - prompt, + request, schema, sampleDocuments, - }: { - databaseName: string; - collectionName: string; - prompt: string; - schema?: string; - sampleDocuments?: Document[]; - }): Promise { + }: QueryPromptArgs): Promise { + let prompt = request.prompt; prompt += `\nDatabase name: ${databaseName}\n`; prompt += `Collection name: ${collectionName}\n`; if (schema) { @@ -71,45 +72,10 @@ db.getCollection('');\n`; }); } - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.User(prompt); - } - - static async buildMessages({ - context, - request, - databaseName, - collectionName, - schema, - sampleDocuments, - connectionNames, - }: { - request: { - prompt: string; - }; - context: vscode.ChatContext; - databaseName: string; - collectionName: string; - schema?: string; - sampleDocuments?: Document[]; - connectionNames: string[]; - }): Promise { - const messages = [ - QueryPrompt.getAssistantPrompt(), - ...getHistoryMessages({ context, connectionNames }), - await QueryPrompt.getUserPrompt({ - databaseName, - collectionName, - prompt: request.prompt, - schema, - sampleDocuments, - }), - ]; - - return messages; + return prompt; } - static getEmptyRequestResponse(): string { + get emptyRequestResponse(): string { return vscode.l10n.t( 'Please specify a question when using this command. Usage: @MongoDB /query find documents where "name" contains "database".' ); diff --git a/src/participant/prompts/schema.ts b/src/participant/prompts/schema.ts index ffbe6ba61..895f99568 100644 --- a/src/participant/prompts/schema.ts +++ b/src/participant/prompts/schema.ts @@ -1,6 +1,4 @@ -import * as vscode from 'vscode'; - -import { getHistoryMessages } from './history'; +import { PromptBase, type PromptArgsBase } from './promptBase'; export const DOCUMENTS_TO_SAMPLE_FOR_SCHEMA_PROMPT = 100; @@ -8,78 +6,37 @@ export type OpenSchemaCommandArgs = { schema: string; }; -export class SchemaPrompt { - static getAssistantPrompt({ - amountOfDocumentsSampled, - }: { - amountOfDocumentsSampled: number; - }): vscode.LanguageModelChatMessage { - const prompt = `You are a senior engineer who describes the schema of documents in a MongoDB database. +export interface SchemaPromptArgs extends PromptArgsBase { + databaseName: string; + collectionName: string; + schema: string; + amountOfDocumentsSampled: number; + connectionNames: string[]; +} + +export class SchemaPrompt extends PromptBase { + getAssistantPrompt({ amountOfDocumentsSampled }: SchemaPromptArgs): string { + return `You are a senior engineer who describes the schema of documents in a MongoDB database. The schema is generated from a sample of documents in the user's collection. -You must follows these rules. +You must follow these rules. Rule 1: Try to be as concise as possible. Rule 2: Pay attention to the JSON schema. Rule 3: Mention the amount of documents sampled in your response. Amount of documents sampled: ${amountOfDocumentsSampled}.`; - - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.Assistant(prompt); } - static getUserPrompt({ + getUserPrompt({ databaseName, collectionName, - prompt, + request, schema, - }: { - databaseName: string; - collectionName: string; - prompt: string; - schema: string; - }): vscode.LanguageModelChatMessage { - const userInput = `${ + }: SchemaPromptArgs): Promise { + const prompt = request.prompt; + return Promise.resolve(`${ prompt ? `The user provided additional information: "${prompt}"\n` : '' }Database name: ${databaseName} Collection name: ${collectionName} Schema: -${schema}`; - - // eslint-disable-next-line new-cap - return vscode.LanguageModelChatMessage.User(userInput); - } - - static buildMessages({ - context, - databaseName, - collectionName, - schema, - amountOfDocumentsSampled, - request, - connectionNames, - }: { - request: { - prompt: string; - }; - databaseName: string; - collectionName: string; - schema: string; - amountOfDocumentsSampled: number; - context: vscode.ChatContext; - connectionNames: string[]; - }): vscode.LanguageModelChatMessage[] { - const messages = [ - SchemaPrompt.getAssistantPrompt({ - amountOfDocumentsSampled, - }), - ...getHistoryMessages({ context, connectionNames }), - SchemaPrompt.getUserPrompt({ - prompt: request.prompt, - databaseName, - collectionName, - schema, - }), - ]; - - return messages; +${schema}`); } } diff --git a/src/participant/sampleDocuments.ts b/src/participant/sampleDocuments.ts index bfefa310b..4945839c0 100644 --- a/src/participant/sampleDocuments.ts +++ b/src/participant/sampleDocuments.ts @@ -32,9 +32,9 @@ export async function getStringifiedSampleDocuments({ sampleDocuments, }: { prompt: string; - sampleDocuments: Document[]; + sampleDocuments?: Document[]; }): Promise { - if (!sampleDocuments.length) { + if (!sampleDocuments?.length) { return ''; } @@ -44,9 +44,8 @@ export async function getStringifiedSampleDocuments({ } let additionToPrompt: Document[] | Document = sampleDocuments; - let promptInputTokens = await model.countTokens( - prompt + toJSString(sampleDocuments) - ); + let promptInputTokens = + (await model.countTokens(prompt + toJSString(sampleDocuments))) || 0; // First check the length of all stringified sample documents. // If the resulting prompt is too large, proceed with only 1 sample document. @@ -59,11 +58,14 @@ export async function getStringifiedSampleDocuments({ } const stringifiedDocuments = toJSString(additionToPrompt); - promptInputTokens = await model.countTokens(prompt + stringifiedDocuments); + + // TODO: model.countTokens will sometimes return undefined - at least in tests. We should investigate why. + promptInputTokens = + (await model.countTokens(prompt + stringifiedDocuments)) || 0; // Add sample documents to the prompt only when it fits in the context window. if (promptInputTokens <= model.maxInputTokens) { - return `Sample document${ + return `\nSample document${ Array.isArray(additionToPrompt) ? 's' : '' }: ${stringifiedDocuments}\n`; } diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index 660b0a169..77db0244b 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -12,16 +12,13 @@ import { loadFixturesToDB, reloadFixture } from './fixtures/fixture-loader'; import type { Fixtures } from './fixtures/fixture-loader'; import { AIBackend } from './ai-backend'; import type { ChatCompletion } from './ai-backend'; -import { GenericPrompt } from '../../participant/prompts/generic'; -import { QueryPrompt } from '../../participant/prompts/query'; import { createTestResultsHTMLPage, type TestOutputs, type TestResult, } from './create-test-results-html-page'; -import { NamespacePrompt } from '../../participant/prompts/namespace'; import { runCodeInMessage } from './assertions'; -import { parseForDatabaseAndCollectionName } from '../../participant/participant'; +import { Prompts } from '../../participant/prompts'; const numberOfRunsPerTest = 1; @@ -58,7 +55,10 @@ const namespaceTestCases: TestCase[] = [ userInput: 'How many documents are in the tempReadings collection in the pools database?', assertResult: ({ responseContent }: AssertProps): void => { - const namespace = parseForDatabaseAndCollectionName(responseContent); + const namespace = + Prompts.namespace.extractDatabaseAndCollectionNameFromResponse( + responseContent + ); expect(namespace.databaseName).to.equal('pools'); expect(namespace.collectionName).to.equal('tempReadings'); @@ -69,7 +69,10 @@ const namespaceTestCases: TestCase[] = [ type: 'namespace', userInput: 'How many documents are in the collection?', assertResult: ({ responseContent }: AssertProps): void => { - const namespace = parseForDatabaseAndCollectionName(responseContent); + const namespace = + Prompts.namespace.extractDatabaseAndCollectionNameFromResponse( + responseContent + ); expect(namespace.databaseName).to.equal(undefined); expect(namespace.collectionName).to.equal(undefined); @@ -81,7 +84,10 @@ const namespaceTestCases: TestCase[] = [ userInput: 'How do I create a new user with read write permissions on the orders collection?', assertResult: ({ responseContent }: AssertProps): void => { - const namespace = parseForDatabaseAndCollectionName(responseContent); + const namespace = + Prompts.namespace.extractDatabaseAndCollectionNameFromResponse( + responseContent + ); expect(namespace.databaseName).to.equal(undefined); expect(namespace.collectionName).to.equal('orders'); @@ -93,7 +99,10 @@ const namespaceTestCases: TestCase[] = [ userInput: 'How do I create a new user with read write permissions on the orders db?', assertResult: ({ responseContent }: AssertProps): void => { - const namespace = parseForDatabaseAndCollectionName(responseContent); + const namespace = + Prompts.namespace.extractDatabaseAndCollectionNameFromResponse( + responseContent + ); expect(namespace.databaseName).to.equal('orders'); expect(namespace.collectionName).to.equal(undefined); @@ -311,13 +320,14 @@ const buildMessages = async ({ }): Promise => { switch (testCase.type) { case 'generic': - return GenericPrompt.buildMessages({ + return Prompts.generic.buildMessages({ request: { prompt: testCase.userInput }, context: { history: [] }, + connectionNames: [], }); case 'query': - return await QueryPrompt.buildMessages({ + return await Prompts.query.buildMessages({ request: { prompt: testCase.userInput }, context: { history: [] }, databaseName: testCase.databaseName ?? 'test', @@ -343,7 +353,7 @@ const buildMessages = async ({ }); case 'namespace': - return NamespacePrompt.buildMessages({ + return Prompts.namespace.buildMessages({ request: { prompt: testCase.userInput }, context: { history: [] }, connectionNames: [], diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 6e1e9889e..d52debb46 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -7,7 +7,6 @@ import type { DataService } from 'mongodb-data-service'; import { ObjectId, Int32 } from 'bson'; import ParticipantController, { - parseForDatabaseAndCollectionName, getRunnableContentFromString, } from '../../../participant/participant'; import ConnectionController from '../../../connectionController'; @@ -26,6 +25,9 @@ import { } from '../../../storage/storageController'; import type { LoadedConnection } from '../../../storage/connectionStorage'; import { ChatMetadataStore } from '../../../participant/chatMetadata'; +import { Prompts } from '../../../participant/prompts'; +import { createMarkdownLink } from '../../../participant/markdown'; +import EXTENSION_COMMANDS from '../../../commands'; // The Copilot's model in not available in tests, // therefore we need to mock its methods and returning values. @@ -153,7 +155,7 @@ suite('Participant Controller Test Suite', function () { test('parses a returned by ai text for database and collection name', function () { const text = 'DATABASE_NAME: my \nCOLLECTION_NAME: cats'; const { databaseName, collectionName } = - parseForDatabaseAndCollectionName(text); + Prompts.namespace.extractDatabaseAndCollectionNameFromResponse(text); expect(databaseName).to.be.equal('my'); expect(collectionName).to.be.equal('cats'); }); @@ -1282,6 +1284,219 @@ Schema: }); }); + suite('prompt builders', function () { + test('generic', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + }; + const messages = await Prompts.generic.buildMessages({ + context: chatContextStub, + request: chatRequestMock, + connectionNames: [], + }); + + expect(messages).to.have.lengthOf(2); + expect(messages[0].role).to.equal( + vscode.LanguageModelChatMessageRole.Assistant + ); + expect(messages[1].role).to.equal( + vscode.LanguageModelChatMessageRole.User + ); + }); + + test('query', async function () { + const chatRequestMock = { + prompt: + 'how do I find the number of people whose name starts with "P"?', + command: 'query', + }; + + chatContextStub = { + history: [ + Object.assign(Object.create(vscode.ChatRequestTurn.prototype), { + prompt: 'give me the count of all people in the prod database', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + }), + ], + }; + const messages = await Prompts.query.buildMessages({ + context: chatContextStub, + request: chatRequestMock, + collectionName: 'people', + connectionNames: ['localhost', 'atlas'], + databaseName: 'prod', + sampleDocuments: [ + { + _id: new ObjectId(), + name: 'Peter', + }, + { + _id: new ObjectId(), + name: 'John', + }, + ], + schema: ` + { + _id: ObjectId, + name: String + } + `, + }); + + expect(messages).to.have.lengthOf(3); + + // Assistant prompt + expect(messages[0].role).to.equal( + vscode.LanguageModelChatMessageRole.Assistant + ); + + // History + expect(messages[1].role).to.equal( + vscode.LanguageModelChatMessageRole.User + ); + expect(messages[1].content).to.equal( + 'give me the count of all people in the prod database' + ); + + // Actual user prompt + expect(messages[2].role).to.equal( + vscode.LanguageModelChatMessageRole.User + ); + }); + + test('schema', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'schema', + }; + + const databaseName = 'dbOne'; + const collectionName = 'collOne'; + const schema = ` + { + _id: ObjectId, + name: String + } + `; + const messages = await Prompts.schema.buildMessages({ + context: chatContextStub, + request: chatRequestMock, + amountOfDocumentsSampled: 3, + collectionName, + databaseName, + schema, + connectionNames: [], + }); + + expect(messages).to.have.lengthOf(2); + expect(messages[0].role).to.equal( + vscode.LanguageModelChatMessageRole.Assistant + ); + expect(messages[0].content).to.include('Amount of documents sampled: 3'); + + expect(messages[1].role).to.equal( + vscode.LanguageModelChatMessageRole.User + ); + expect(messages[1].content).to.include(databaseName); + expect(messages[1].content).to.include(collectionName); + expect(messages[1].content).to.include(schema); + }); + + test('namespace', async function () { + const chatRequestMock = { + prompt: 'find all docs by a name example', + command: 'query', + }; + const messages = await Prompts.namespace.buildMessages({ + context: chatContextStub, + request: chatRequestMock, + connectionNames: [], + }); + + expect(messages).to.have.lengthOf(2); + expect(messages[0].role).to.equal( + vscode.LanguageModelChatMessageRole.Assistant + ); + expect(messages[1].role).to.equal( + vscode.LanguageModelChatMessageRole.User + ); + }); + + test('removes askForConnect messages from history', async function () { + // The user is responding to an `askToConnect` message, so the prompt is just the + // name of the connection + const chatRequestMock = { + prompt: 'localhost', + command: 'query', + }; + + chatContextStub = { + history: [ + Object.assign(Object.create(vscode.ChatRequestTurn.prototype), { + prompt: 'give me the count of all people in the prod database', + command: 'query', + references: [], + participant: CHAT_PARTICIPANT_ID, + }), + Object.assign(Object.create(vscode.ChatResponseTurn.prototype), { + participant: CHAT_PARTICIPANT_ID, + response: [ + { + value: { + value: `Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against. + + ${createMarkdownLink({ + commandId: EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, + name: 'localhost', + data: {}, + })} + ${createMarkdownLink({ + commandId: EXTENSION_COMMANDS.CONNECT_WITH_PARTICIPANT, + name: 'atlas', + data: {}, + })}`, + } as vscode.MarkdownString, + }, + ], + command: 'query', + result: { + metadata: { + intent: 'askToConnect', + chatId: 'abc', + }, + }, + }), + ], + }; + + const messages = await Prompts.query.buildMessages({ + context: chatContextStub, + request: chatRequestMock, + collectionName: 'people', + connectionNames: ['localhost', 'atlas'], + databaseName: 'prod', + sampleDocuments: [], + }); + + expect(messages.length).to.equal(2); + expect(messages[0].role).to.equal( + vscode.LanguageModelChatMessageRole.Assistant + ); + + // We don't expect history because we're removing the askForConnect message as well + // as the user response to it. Therefore the actual user prompt should be the first + // message that we supplied in the history. + expect(messages[1].role).to.equal( + vscode.LanguageModelChatMessageRole.User + ); + expect(messages[1].content).to.contain( + 'give me the count of all people in the prod database' + ); + }); + }); + suite('telemetry', function () { test('reports positive user feedback', async function () { await testParticipantController.handleUserFeedback({ From 118aee07beda6586a36515e337a9036720ae4da5 Mon Sep 17 00:00:00 2001 From: Rhys Date: Thu, 26 Sep 2024 11:02:43 -0400 Subject: [PATCH 36/45] feat(participant): route generic prompt by intent and update generic prompt VSCODE-572 (#830) --- src/participant/participant.ts | 91 ++++++- src/participant/prompts/generic.ts | 18 +- src/participant/prompts/index.ts | 5 +- src/participant/prompts/intent.ts | 50 ++++ .../ai-accuracy-tests/ai-accuracy-tests.ts | 237 ++++++++++++++++-- .../create-test-results-html-page.ts | 18 +- .../ai-accuracy-tests/fixtures/recipes.ts | 15 ++ .../suite/participant/participant.test.ts | 61 ++++- 8 files changed, 456 insertions(+), 39 deletions(-) create mode 100644 src/participant/prompts/intent.ts diff --git a/src/participant/participant.ts b/src/participant/participant.ts index dc9e94a17..8edf5dc44 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -38,6 +38,7 @@ import { } from '../telemetry/telemetryService'; import { DocsChatbotAIService } from './docsChatbotAIService'; import type TelemetryService from '../telemetry/telemetryService'; +import { IntentPrompt, type PromptIntent } from './prompts/intent'; const log = createLogger('participant'); @@ -214,8 +215,7 @@ export default class ParticipantController { } } - // @MongoDB what is mongodb? - async handleGenericRequest( + async _handleRoutedGenericRequest( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, @@ -241,6 +241,93 @@ export default class ParticipantController { return genericRequestChatResult(context.history); } + async _routeRequestToHandler({ + context, + promptIntent, + request, + stream, + token, + }: { + context: vscode.ChatContext; + promptIntent: Omit; + request: vscode.ChatRequest; + stream: vscode.ChatResponseStream; + token: vscode.CancellationToken; + }): Promise { + switch (promptIntent) { + case 'Query': + return this.handleQueryRequest(request, context, stream, token); + case 'Docs': + return this.handleDocsRequest(request, context, stream, token); + case 'Schema': + return this.handleSchemaRequest(request, context, stream, token); + case 'Code': + return this.handleQueryRequest(request, context, stream, token); + default: + return this._handleRoutedGenericRequest( + request, + context, + stream, + token + ); + } + } + + async _getIntentFromChatRequest({ + context, + request, + token, + }: { + context: vscode.ChatContext; + request: vscode.ChatRequest; + token: vscode.CancellationToken; + }): Promise { + const messages = await Prompts.intent.buildMessages({ + connectionNames: this._getConnectionNames(), + request, + context, + }); + + const responseContent = await this.getChatResponseContent({ + messages, + token, + }); + + return IntentPrompt.getIntentFromModelResponse(responseContent); + } + + async handleGenericRequest( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ): Promise { + // We "prompt chain" to handle the generic requests. + // First we ask the model to parse for intent. + // If there is an intent, we can route it to one of the handlers (/commands). + // When there is no intention or it's generic we handle it with a generic handler. + const promptIntent = await this._getIntentFromChatRequest({ + context, + request, + token, + }); + + if (token.isCancellationRequested) { + return this._handleCancelledRequest({ + context, + stream, + }); + } + + return this._routeRequestToHandler({ + context, + promptIntent, + request, + stream, + token, + }); + } + async connectWithParticipant({ id, command, diff --git a/src/participant/prompts/generic.ts b/src/participant/prompts/generic.ts index 9b5348790..40b531228 100644 --- a/src/participant/prompts/generic.ts +++ b/src/participant/prompts/generic.ts @@ -1,20 +1,24 @@ import * as vscode from 'vscode'; + import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; export class GenericPrompt extends PromptBase { protected getAssistantPrompt(): string { return `You are a MongoDB expert. -Your task is to help the user craft MongoDB queries and aggregation pipelines that perform their task. -Keep your response concise. -You should suggest queries that are performant and correct. -Respond with markdown, suggest code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. -You can imagine the schema, collection, and database name. -Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax.`; +Your task is to help the user with MongoDB related questions. +When applicable, you may suggest MongoDB code, queries, and aggregation pipelines that perform their task. +Rules: +1. Keep your response concise. +2. You should suggest code that is performant and correct. +3. Respond with markdown. +4. When relevant, provide code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. +5. Use MongoDB shell syntax for code unless the user requests a specific language. +6. If you require additional information to provide a response, ask the user for it. +7. When specifying a database, use the MongoDB syntax use('databaseName').`; } public getEmptyRequestResponse(): string { - // TODO(VSCODE-572): Generic empty response handler return vscode.l10n.t( 'Ask anything about MongoDB, from writing queries to questions about your cluster.' ); diff --git a/src/participant/prompts/index.ts b/src/participant/prompts/index.ts index 2d40beec4..867b057dd 100644 --- a/src/participant/prompts/index.ts +++ b/src/participant/prompts/index.ts @@ -1,11 +1,14 @@ -import { GenericPrompt } from './generic'; import type * as vscode from 'vscode'; + +import { GenericPrompt } from './generic'; +import { IntentPrompt } from './intent'; import { NamespacePrompt } from './namespace'; import { QueryPrompt } from './query'; import { SchemaPrompt } from './schema'; export class Prompts { public static generic = new GenericPrompt(); + public static intent = new IntentPrompt(); public static namespace = new NamespacePrompt(); public static query = new QueryPrompt(); public static schema = new SchemaPrompt(); diff --git a/src/participant/prompts/intent.ts b/src/participant/prompts/intent.ts new file mode 100644 index 000000000..0726f0fc7 --- /dev/null +++ b/src/participant/prompts/intent.ts @@ -0,0 +1,50 @@ +import type { PromptArgsBase } from './promptBase'; +import { PromptBase } from './promptBase'; + +export type PromptIntent = 'Query' | 'Schema' | 'Docs' | 'Default'; + +export class IntentPrompt extends PromptBase { + protected getAssistantPrompt(): string { + return `You are a MongoDB expert. +Your task is to help guide a conversation with a user to the correct handler. +You will be provided a conversation and your task is to determine the intent of the user. +The intent handlers are: +- Query +- Schema +- Docs +- Default +Rules: +1. Respond only with the intent handler. +2. Use the "Query" intent handler when the user is asking for code that relates to a specific collection. +3. Use the "Docs" intent handler when the user is asking a question that involves MongoDB documentation. +4. Use the "Schema" intent handler when the user is asking for the schema or shape of documents of a specific collection. +5. Use the "Default" intent handler when a user is asking for code that does NOT relate to a specific collection. +6. Use the "Default" intent handler for everything that may not be handled by another handler. +7. If you are uncertain of the intent, use the "Default" intent handler. + +Example: +User: How do I create an index in my pineapples collection? +Response: +Query + +Example: +User: +What is $vectorSearch? +Response: +Docs`; + } + + static getIntentFromModelResponse(response: string): PromptIntent { + response = response.trim(); + switch (response) { + case 'Query': + return 'Query'; + case 'Schema': + return 'Schema'; + case 'Docs': + return 'Docs'; + default: + return 'Default'; + } + } +} diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index 77db0244b..c3135400d 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -17,7 +17,7 @@ import { type TestOutputs, type TestResult, } from './create-test-results-html-page'; -import { runCodeInMessage } from './assertions'; +import { anyOf, runCodeInMessage } from './assertions'; import { Prompts } from '../../participant/prompts'; const numberOfRunsPerTest = 1; @@ -34,7 +34,7 @@ type AssertProps = { type TestCase = { testCase: string; - type: 'generic' | 'query' | 'namespace'; + type: 'intent' | 'generic' | 'query' | 'namespace'; userInput: string; // Some tests can edit the documents in a collection. // As we want tests to run in isolation this flag will cause the fixture @@ -48,7 +48,9 @@ type TestCase = { only?: boolean; // Translates to mocha's it.only so only this test will run. }; -const namespaceTestCases: TestCase[] = [ +const namespaceTestCases: (TestCase & { + type: 'namespace'; +})[] = [ { testCase: 'Namespace included in query', type: 'namespace', @@ -110,7 +112,9 @@ const namespaceTestCases: TestCase[] = [ }, ]; -const queryTestCases: TestCase[] = [ +const queryTestCases: (TestCase & { + type: 'query'; +})[] = [ { testCase: 'Basic query', type: 'query', @@ -245,9 +249,177 @@ const queryTestCases: TestCase[] = [ expect(output.data?.result?.content[0].collectors).to.include('Monkey'); }, }, + { + testCase: 'Complex aggregation with string and number manipulation', + type: 'query', + databaseName: 'CookBook', + collectionName: 'recipes', + userInput: + 'what percentage of recipes have "salt" in their ingredients? "ingredients" is a field ' + + 'with an array of strings of the ingredients. Only consider recipes ' + + 'that have the "difficulty Medium or Easy. Return is as a string named "saltPercentage" like ' + + '"75%", rounded to the nearest whole number.', + assertResult: async ({ + responseContent, + connectionString, + }: AssertProps): Promise => { + const output = await runCodeInMessage(responseContent, connectionString); + + anyOf([ + (): void => { + const lines = responseContent.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + + expect(lastLine).to.include('saltPercentage'); + expect(output.data?.result?.content).to.include('67%'); + }, + (): void => { + expect(output.printOutput[output.printOutput.length - 1]).to.equal( + "{ saltPercentage: '67%' }" + ); + }, + (): void => { + expect(output.data?.result?.content[0].saltPercentage).to.equal( + '67%' + ); + }, + ])(null); + }, + }, ]; -const testCases: TestCase[] = [...namespaceTestCases, ...queryTestCases]; +const intentTestCases: (TestCase & { + type: 'intent'; +})[] = [ + { + testCase: 'Docs intent', + type: 'intent', + userInput: + 'Where can I find more information on how to connect to MongoDB?', + assertResult: ({ responseContent }: AssertProps): void => { + expect(responseContent).to.equal('Docs'); + }, + }, + { + testCase: 'Docs intent 2', + type: 'intent', + userInput: 'What are the options when creating an aggregation cursor?', + assertResult: ({ responseContent }: AssertProps): void => { + expect(responseContent).to.equal('Docs'); + }, + }, + { + testCase: 'Query intent', + type: 'intent', + userInput: + 'which collectors specialize only in mint items? and are located in London or New York? an array of their names in a field called collectors', + assertResult: ({ responseContent }: AssertProps): void => { + expect(responseContent).to.equal('Query'); + }, + }, + { + testCase: 'Schema intent', + type: 'intent', + userInput: 'What do the documents in the collection pineapple look like?', + assertResult: ({ responseContent }: AssertProps): void => { + expect(responseContent).to.equal('Schema'); + }, + }, + { + testCase: 'Default/Generic intent 1', + type: 'intent', + userInput: 'How can I connect to MongoDB?', + assertResult: ({ responseContent }: AssertProps): void => { + expect(responseContent).to.equal('Default'); + }, + }, + { + testCase: 'Default/Generic intent 2', + type: 'intent', + userInput: 'What is the size breakdown of all of the databases?', + assertResult: ({ responseContent }: AssertProps): void => { + expect(responseContent).to.equal('Default'); + }, + }, +]; + +const genericTestCases: (TestCase & { + type: 'generic'; +})[] = [ + { + testCase: 'Database meta data question', + type: 'generic', + userInput: + 'How do I print the name and size of the largest database? Using the print function', + assertResult: async ({ + responseContent, + connectionString, + }: AssertProps): Promise => { + const output = await runCodeInMessage(responseContent, connectionString); + const printOutput = output.printOutput.join(''); + + // Don't check the name since they're all the base 8192. + expect(printOutput).to.include('8192'); + }, + }, + { + testCase: 'Code question with database, collection, and fields named', + type: 'generic', + userInput: + 'How many sightings happened in the "year" "2020" and "2021"? database "UFO" collection "sightings". code to just return the one total number. also, the year is a string', + assertResult: async ({ + responseContent, + connectionString, + }: AssertProps): Promise => { + const output = await runCodeInMessage(responseContent, connectionString); + anyOf([ + (): void => { + expect(output.printOutput.join('')).to.equal('2'); + }, + (): void => { + expect(output.data?.result?.content).to.equal('2'); + }, + (): void => { + expect(output.data?.result?.content).to.equal(2); + }, + (): void => { + expect( + Object.entries(output.data?.result?.content[0])[0][1] + ).to.equal(2); + }, + (): void => { + expect( + Object.entries(output.data?.result?.content[0])[0][1] + ).to.equal('2'); + }, + ])(null); + }, + }, + { + testCase: 'Complex aggregation code generation', + type: 'generic', + userInput: + 'what percentage of recipes have "salt" in their ingredients? "ingredients" is a field ' + + 'with an array of strings of the ingredients. Only consider recipes ' + + 'that have the "difficulty Medium or Easy. Return is as a string named "saltPercentage" like ' + + '"75%", rounded to the nearest whole number. db CookBook, collection recipes', + assertResult: async ({ + responseContent, + connectionString, + }: AssertProps): Promise => { + const output = await runCodeInMessage(responseContent, connectionString); + + expect(output.data?.result?.content[0].saltPercentage).to.equal('67%'); + }, + }, +]; + +const testCases: TestCase[] = [ + ...namespaceTestCases, + ...queryTestCases, + ...intentTestCases, + ...genericTestCases, +]; const projectRoot = path.join(__dirname, '..', '..', '..'); @@ -319,6 +491,13 @@ const buildMessages = async ({ fixtures: Fixtures; }): Promise => { switch (testCase.type) { + case 'intent': + return Prompts.intent.buildMessages({ + request: { prompt: testCase.userInput }, + context: { history: [] }, + connectionNames: [], + }); + case 'generic': return Prompts.generic.buildMessages({ request: { prompt: testCase.userInput }, @@ -473,12 +652,14 @@ describe('AI Accuracy Tests', function () { testFunction( `should pass for input: "${testCase.userInput}" if average accuracy is above threshold`, - // eslint-disable-next-line no-loop-func + // eslint-disable-next-line no-loop-func, complexity async function () { console.log(`Starting test run of ${testCase.testCase}.`); const testRunDidSucceed: boolean[] = []; - const successFullRunStats: { + // Successful and unsuccessful runs are both tracked as long as the model + // returns a response. + const runStats: { promptTokens: number; completionTokens: number; executionTimeMS: number; @@ -505,12 +686,15 @@ describe('AI Accuracy Tests', function () { } const startTime = Date.now(); + let responseContent: ChatCompletion | undefined; + let executionTimeMS = 0; try { - const responseContent = await runTest({ + responseContent = await runTest({ testCase, aiBackend, fixtures, }); + executionTimeMS = Date.now() - startTime; testOutputs[testCase.testCase].outputs.push( responseContent.content ); @@ -521,11 +705,6 @@ describe('AI Accuracy Tests', function () { mongoClient, }); - successFullRunStats.push({ - completionTokens: responseContent.usageStats.completionTokens, - promptTokens: responseContent.usageStats.promptTokens, - executionTimeMS: Date.now() - startTime, - }); success = true; console.log( @@ -538,6 +717,18 @@ describe('AI Accuracy Tests', function () { ); } + if ( + responseContent && + responseContent?.usageStats?.completionTokens > 0 && + executionTimeMS !== 0 + ) { + runStats.push({ + completionTokens: responseContent.usageStats.completionTokens, + promptTokens: responseContent.usageStats.promptTokens, + executionTimeMS, + }); + } + testRunDidSucceed.push(success); } @@ -558,21 +749,19 @@ describe('AI Accuracy Tests', function () { Accuracy: averageAccuracy, Pass: didFail ? '✗' : '✓', 'Avg Execution Time (ms)': - successFullRunStats.length > 0 - ? successFullRunStats.reduce((a, b) => a + b.executionTimeMS, 0) / - successFullRunStats.length + runStats.length > 0 + ? runStats.reduce((a, b) => a + b.executionTimeMS, 0) / + runStats.length : 0, 'Avg Prompt Tokens': - successFullRunStats.length > 0 - ? successFullRunStats.reduce((a, b) => a + b.promptTokens, 0) / - successFullRunStats.length + runStats.length > 0 + ? runStats.reduce((a, b) => a + b.promptTokens, 0) / + runStats.length : 0, 'Avg Completion Tokens': - successFullRunStats.length > 0 - ? successFullRunStats.reduce( - (a, b) => a + b.completionTokens, - 0 - ) / successFullRunStats.length + runStats.length > 0 + ? runStats.reduce((a, b) => a + b.completionTokens, 0) / + runStats.length : 0, }); diff --git a/src/test/ai-accuracy-tests/create-test-results-html-page.ts b/src/test/ai-accuracy-tests/create-test-results-html-page.ts index f58a50950..c3774a59e 100644 --- a/src/test/ai-accuracy-tests/create-test-results-html-page.ts +++ b/src/test/ai-accuracy-tests/create-test-results-html-page.ts @@ -23,6 +23,9 @@ export type TestOutputs = { [testName: string]: TestOutput; }; +const createTestLinkId = (testName: string): string => + encodeURIComponent(testName.replace(/ /g, '-')); + function getTestResultsTable(testResults: TestResult[]): string { const headers = Object.keys(testResults[0]) .map((key) => ``) @@ -30,8 +33,15 @@ function getTestResultsTable(testResults: TestResult[]): string { const resultRows = testResults .map((result) => { - const row = Object.values(result) - .map((value) => ``) + const row = Object.entries(result) + .map( + ([field, value]) => + `` + ) .join(''); return `${row}`; }) @@ -56,7 +66,9 @@ function getTestOutputTables(testOutputs: TestOutputs): string { .map((out) => ``) .join(''); return ` -

${testName} [${output.testType}]

+

Prompt: ${output.prompt}

${key}${key}${value}${ + field === 'Test' + ? `${value}` + : value + }
${out}
diff --git a/src/test/ai-accuracy-tests/fixtures/recipes.ts b/src/test/ai-accuracy-tests/fixtures/recipes.ts index efb347bb3..f9e8d7346 100644 --- a/src/test/ai-accuracy-tests/fixtures/recipes.ts +++ b/src/test/ai-accuracy-tests/fixtures/recipes.ts @@ -12,6 +12,7 @@ const recipes: Fixture = { 'tomato sauce', 'onions', 'garlic', + 'salt', ], preparationTime: 60, difficulty: 'Medium', @@ -23,6 +24,19 @@ const recipes: Fixture = { preparationTime: 10, difficulty: 'Easy', }, + { + title: 'Pineapple', + ingredients: ['pineapple'], + preparationTime: 5, + difficulty: 'Very Hard', + }, + { + title: 'Pizza', + ingredients: ['dough', 'tomato sauce', 'mozzarella cheese', 'basil'], + optionalIngredients: ['pineapple'], + preparationTime: 40, + difficulty: 'Medium', + }, { title: 'Beef Wellington', ingredients: [ @@ -30,6 +44,7 @@ const recipes: Fixture = { 'mushroom duxelles', 'puff pastry', 'egg wash', + 'salt', ], preparationTime: 120, difficulty: 'Hard', diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index d52debb46..557fbd320 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -437,14 +437,71 @@ suite('Participant Controller Test Suite', function () { }); suite('generic command', function () { - test('generates a query', async function () { + suite('when the intent is recognized', function () { + beforeEach(function () { + sendRequestStub.onCall(0).resolves({ + text: ['Schema'], + }); + }); + + test('routes to the appropriate handler', async function () { + const chatRequestMock = { + prompt: + 'what is the shape of the documents in the pineapple collection?', + command: undefined, + references: [], + }; + const res = await invokeChatHandler(chatRequestMock); + + expect(sendRequestStub).to.have.been.calledTwice; + const intentRequest = sendRequestStub.firstCall.args[0]; + expect(intentRequest).to.have.length(2); + expect(intentRequest[0].content).to.include( + 'Your task is to help guide a conversation with a user to the correct handler.' + ); + expect(intentRequest[1].content).to.equal( + 'what is the shape of the documents in the pineapple collection?' + ); + const genericRequest = sendRequestStub.secondCall.args[0]; + expect(genericRequest).to.have.length(2); + expect(genericRequest[0].content).to.include( + 'Parse all user messages to find a database name and a collection name.' + ); + expect(genericRequest[1].content).to.equal( + 'what is the shape of the documents in the pineapple collection?' + ); + + expect(res?.metadata.intent).to.equal('askForNamespace'); + }); + }); + + test('default handler asks for intent and shows code run actions', async function () { const chatRequestMock = { prompt: 'how to find documents in my collection?', command: undefined, references: [], }; - await invokeChatHandler(chatRequestMock); + const res = await invokeChatHandler(chatRequestMock); + + expect(sendRequestStub).to.have.been.calledTwice; + const intentRequest = sendRequestStub.firstCall.args[0]; + expect(intentRequest).to.have.length(2); + expect(intentRequest[0].content).to.include( + 'Your task is to help guide a conversation with a user to the correct handler.' + ); + expect(intentRequest[1].content).to.equal( + 'how to find documents in my collection?' + ); + const genericRequest = sendRequestStub.secondCall.args[0]; + expect(genericRequest).to.have.length(2); + expect(genericRequest[0].content).to.include( + 'Your task is to help the user with MongoDB related questions.' + ); + expect(genericRequest[1].content).to.equal( + 'how to find documents in my collection?' + ); + expect(res?.metadata.intent).to.equal('generic'); expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ command: 'mdb.runParticipantQuery', title: '▶️ Run', From 7eef0e74ea5a8d4a2274654d48e353bbf9334fe5 Mon Sep 17 00:00:00 2001 From: Rhys Date: Thu, 26 Sep 2024 12:09:29 -0400 Subject: [PATCH 37/45] fix(chat): update response handling to stream and inline code block parsing VSCODE-620 (#835) --- src/participant/constants.ts | 5 + src/participant/participant.ts | 225 +++++++++++------- src/participant/prompts/generic.ts | 4 +- src/participant/prompts/intent.ts | 2 +- src/participant/prompts/query.ts | 13 +- src/participant/streamParsing.ts | 95 ++++++++ src/test/ai-accuracy-tests/assertions.ts | 17 +- .../participant/asyncIterableFromArray.ts | 24 ++ .../suite/participant/participant.test.ts | 16 +- .../suite/participant/streamParsing.test.ts | 219 +++++++++++++++++ 10 files changed, 508 insertions(+), 112 deletions(-) create mode 100644 src/participant/streamParsing.ts create mode 100644 src/test/suite/participant/asyncIterableFromArray.ts create mode 100644 src/test/suite/participant/streamParsing.test.ts diff --git a/src/participant/constants.ts b/src/participant/constants.ts index b43b1332f..90fd81490 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -14,6 +14,11 @@ export type ParticipantResponseType = | 'askToConnect' | 'askForNamespace'; +export const codeBlockIdentifier = { + start: '```javascript', + end: '```', +}; + interface Metadata { intent: Exclude; chatId: string; diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 8edf5dc44..8d57f0726 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -21,6 +21,7 @@ import { docsRequestChatResult, schemaRequestChatResult, createCancelledRequestChatResult, + codeBlockIdentifier, } from './constants'; import { SchemaFormatter } from './schema'; import { getSimplifiedSampleDocuments } from './sampleDocuments'; @@ -38,7 +39,8 @@ import { } from '../telemetry/telemetryService'; import { DocsChatbotAIService } from './docsChatbotAIService'; import type TelemetryService from '../telemetry/telemetryService'; -import { IntentPrompt, type PromptIntent } from './prompts/intent'; +import { processStreamWithIdentifiers } from './streamParsing'; +import type { PromptIntent } from './prompts/intent'; const log = createLogger('participant'); @@ -59,16 +61,6 @@ export type ParticipantCommand = '/query' | '/schema' | '/docs'; const MAX_MARKDOWN_LIST_LENGTH = 10; -export function getRunnableContentFromString(text: string): string { - const matchedJSresponseContent = text.match(/```javascript((.|\n)*)```/); - - const code = - matchedJSresponseContent && matchedJSresponseContent.length > 1 - ? matchedJSresponseContent[1] - : ''; - return code.trim(); -} - export default class ParticipantController { _participant?: vscode.ChatParticipant; _connectionController: ConnectionController; @@ -171,48 +163,113 @@ export default class ParticipantController { }); } - async getChatResponseContent({ + async _getChatResponse({ messages, token, }: { messages: vscode.LanguageModelChatMessage[]; token: vscode.CancellationToken; - }): Promise { + }): Promise { const model = await getCopilotModel(); - let responseContent = ''; - if (model) { - const chatResponse = await model.sendRequest(messages, {}, token); - for await (const fragment of chatResponse.text) { - responseContent += fragment; - } + + if (!model) { + throw new Error('Copilot model not found'); } - return responseContent; + return await model.sendRequest(messages, {}, token); } - _streamRunnableContentActions({ - responseContent, + async streamChatResponse({ + messages, stream, + token, }: { - responseContent: string; + messages: vscode.LanguageModelChatMessage[]; + stream: vscode.ChatResponseStream; + token: vscode.CancellationToken; + }): Promise { + const chatResponse = await this._getChatResponse({ + messages, + token, + }); + for await (const fragment of chatResponse.text) { + stream.markdown(fragment); + } + } + + _streamCodeBlockActions({ + runnableContent, + stream, + }: { + runnableContent: string; stream: vscode.ChatResponseStream; }): void { - const runnableContent = getRunnableContentFromString(responseContent); - if (runnableContent) { - const commandArgs: RunParticipantQueryCommandArgs = { - runnableContent, - }; - stream.button({ - command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, - title: vscode.l10n.t('▶️ Run'), - arguments: [commandArgs], - }); - stream.button({ - command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, - title: vscode.l10n.t('Open in playground'), - arguments: [commandArgs], - }); + runnableContent = runnableContent.trim(); + + if (!runnableContent) { + return; } + + const commandArgs: RunParticipantQueryCommandArgs = { + runnableContent, + }; + stream.button({ + command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, + title: vscode.l10n.t('▶️ Run'), + arguments: [commandArgs], + }); + stream.button({ + command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + title: vscode.l10n.t('Open in playground'), + arguments: [commandArgs], + }); + } + + async streamChatResponseContentWithCodeActions({ + messages, + stream, + token, + }: { + messages: vscode.LanguageModelChatMessage[]; + stream: vscode.ChatResponseStream; + token: vscode.CancellationToken; + }): Promise { + const chatResponse = await this._getChatResponse({ + messages, + token, + }); + + await processStreamWithIdentifiers({ + processStreamFragment: (fragment: string) => { + stream.markdown(fragment); + }, + onStreamIdentifier: (content: string) => { + this._streamCodeBlockActions({ runnableContent: content, stream }); + }, + inputIterable: chatResponse.text, + identifier: codeBlockIdentifier, + }); + } + + // This will stream all of the response content and create a string from it. + // It should only be used when the entire response is needed at one time. + async getChatResponseContent({ + messages, + token, + }: { + messages: vscode.LanguageModelChatMessage[]; + token: vscode.CancellationToken; + }): Promise { + let responseContent = ''; + const chatResponse = await this._getChatResponse({ + messages, + token, + }); + for await (const fragment of chatResponse.text) { + responseContent += fragment; + } + + return responseContent; } async _handleRoutedGenericRequest( @@ -227,14 +284,9 @@ export default class ParticipantController { connectionNames: this._getConnectionNames(), }); - const responseContent = await this.getChatResponseContent({ + await this.streamChatResponseContentWithCodeActions({ messages, token, - }); - stream.markdown(responseContent); - - this._streamRunnableContentActions({ - responseContent, stream, }); @@ -293,7 +345,7 @@ export default class ParticipantController { token, }); - return IntentPrompt.getIntentFromModelResponse(responseContent); + return Prompts.intent.getIntentFromModelResponse(responseContent); } async handleGenericRequest( @@ -1001,11 +1053,11 @@ export default class ParticipantController { connectionNames: this._getConnectionNames(), ...(sampleDocuments ? { sampleDocuments } : {}), }); - const responseContent = await this.getChatResponseContent({ + await this.streamChatResponse({ messages, + stream, token, }); - stream.markdown(responseContent); stream.button({ command: EXTENSION_COMMANDS.PARTICIPANT_OPEN_RAW_SCHEMA_OUTPUT, @@ -1104,16 +1156,11 @@ export default class ParticipantController { connectionNames: this._getConnectionNames(), ...(sampleDocuments ? { sampleDocuments } : {}), }); - const responseContent = await this.getChatResponseContent({ - messages, - token, - }); - - stream.markdown(responseContent); - this._streamRunnableContentActions({ - responseContent, + await this.streamChatResponseContentWithCodeActions({ + messages, stream, + token, }); return queryRequestChatResult(context.history); @@ -1181,32 +1228,41 @@ export default class ParticipantController { vscode.ChatResponseStream, vscode.CancellationToken ] - ): Promise<{ - responseContent: string; - responseReferences?: Reference[]; - }> { - const [request, context, , token] = args; + ): Promise { + const [request, context, stream, token] = args; const messages = await Prompts.generic.buildMessages({ request, context, connectionNames: this._getConnectionNames(), }); - const responseContent = await this.getChatResponseContent({ + await this.streamChatResponseContentWithCodeActions({ messages, + stream, token, }); - const responseReferences = [ - { + + this._streamResponseReference({ + reference: { url: MONGODB_DOCS_LINK, title: 'View MongoDB documentation', }, - ]; + stream, + }); + } - return { - responseContent, - responseReferences, - }; + _streamResponseReference({ + reference, + stream, + }: { + reference: Reference; + stream: vscode.ChatResponseStream; + }): void { + const link = new vscode.MarkdownString( + `- [${reference.title}](${reference.url})\n` + ); + link.supportHtml = true; + stream.markdown(link); } async handleDocsRequest( @@ -1235,6 +1291,19 @@ export default class ParticipantController { token, stream, }); + + if (docsResult.responseReferences) { + for (const reference of docsResult.responseReferences) { + this._streamResponseReference({ + reference, + stream, + }); + } + } + + if (docsResult.responseContent) { + stream.markdown(docsResult.responseContent); + } } catch (error) { // If the docs chatbot API is not available, fall back to Copilot’s LLM and include // the MongoDB documentation link for users to go to our documentation site directly. @@ -1255,25 +1324,7 @@ export default class ParticipantController { } ); - docsResult = await this._handleDocsRequestWithCopilot(...args); - } - - if (docsResult.responseContent) { - stream.markdown(docsResult.responseContent); - this._streamRunnableContentActions({ - responseContent: docsResult.responseContent, - stream, - }); - } - - if (docsResult.responseReferences) { - for (const ref of docsResult.responseReferences) { - const link = new vscode.MarkdownString( - `- [${ref.title}](${ref.url})\n` - ); - link.supportHtml = true; - stream.markdown(link); - } + await this._handleDocsRequestWithCopilot(...args); } return docsRequestChatResult({ diff --git a/src/participant/prompts/generic.ts b/src/participant/prompts/generic.ts index 40b531228..2112233da 100644 --- a/src/participant/prompts/generic.ts +++ b/src/participant/prompts/generic.ts @@ -3,6 +3,8 @@ import * as vscode from 'vscode'; import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; +import { codeBlockIdentifier } from '../constants'; + export class GenericPrompt extends PromptBase { protected getAssistantPrompt(): string { return `You are a MongoDB expert. @@ -12,7 +14,7 @@ Rules: 1. Keep your response concise. 2. You should suggest code that is performant and correct. 3. Respond with markdown. -4. When relevant, provide code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. +4. When relevant, provide code in a Markdown code block that begins with ${codeBlockIdentifier.start} and ends with ${codeBlockIdentifier.end} 5. Use MongoDB shell syntax for code unless the user requests a specific language. 6. If you require additional information to provide a response, ask the user for it. 7. When specifying a database, use the MongoDB syntax use('databaseName').`; diff --git a/src/participant/prompts/intent.ts b/src/participant/prompts/intent.ts index 0726f0fc7..4d6216afa 100644 --- a/src/participant/prompts/intent.ts +++ b/src/participant/prompts/intent.ts @@ -34,7 +34,7 @@ Response: Docs`; } - static getIntentFromModelResponse(response: string): PromptIntent { + getIntentFromModelResponse(response: string): PromptIntent { response = response.trim(); switch (response) { case 'Query': diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index b7ae5cc26..eff4d29ff 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import type { Document } from 'bson'; import { getStringifiedSampleDocuments } from '../sampleDocuments'; +import { codeBlockIdentifier } from '../constants'; import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; @@ -19,15 +20,15 @@ export class QueryPrompt extends PromptBase { Your task is to help the user craft MongoDB shell syntax code to perform their task. Keep your response concise. You must suggest code that is performant and correct. -Respond with markdown, write code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`. -Respond in MongoDB shell syntax using the \`\`\`javascript code block syntax. +Respond with markdown, write code in a Markdown code block that begins with ${codeBlockIdentifier.start} and ends with ${codeBlockIdentifier.end}. +Respond in MongoDB shell syntax using the ${codeBlockIdentifier.start} code block syntax. Concisely explain the code snippet you have generated. Example 1: User: Documents in the orders db, sales collection, where the date is in 2014 and group the total sales for each product. Response: -\`\`\`javascript +${codeBlockIdentifier.start} use('orders'); db.getCollection('sales').aggregate([ // Find all of the sales that occurred in 2014. @@ -35,15 +36,15 @@ db.getCollection('sales').aggregate([ // Group the total sales for each product. { $group: { _id: '$item', totalSaleAmount: { $sum: { $multiply: [ '$price', '$quantity' ] } } } } ]); -\`\`\` +${codeBlockIdentifier.end} Example 2: User: How do I create an index on the name field in my users collection?. Response: -\`\`\`javascript +${codeBlockIdentifier.start} use('test'); db.getCollection('users').createIndex({ name: 1 }); -\`\`\` +${codeBlockIdentifier.end} MongoDB command to specify database: use(''); diff --git a/src/participant/streamParsing.ts b/src/participant/streamParsing.ts new file mode 100644 index 000000000..93bb5dad9 --- /dev/null +++ b/src/participant/streamParsing.ts @@ -0,0 +1,95 @@ +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * This function, provided a stream of text fragments, will stream the + * content to the provided stream and call the onStreamIdentifier function + * when an identifier is streamed. This is useful for inserting code actions + * into a chat response, whenever a code block has been written. + */ +export async function processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier, +}: { + processStreamFragment: (fragment: string) => void; + onStreamIdentifier: (content: string) => void; + inputIterable: AsyncIterable; + identifier: { + start: string; + end: string; + }; +}): Promise { + const escapedIdentifierStart = escapeRegex(identifier.start); + const escapedIdentifierEnd = escapeRegex(identifier.end); + const regex = new RegExp( + `${escapedIdentifierStart}([\\s\\S]*?)${escapedIdentifierEnd}`, + 'g' + ); + + let contentSinceLastIdentifier = ''; + for await (const fragment of inputIterable) { + contentSinceLastIdentifier += fragment; + + let lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = regex.exec(contentSinceLastIdentifier)) !== null) { + const endIndex = regex.lastIndex; + + // Stream content up to the end of the identifier. + const contentToStream = contentSinceLastIdentifier.slice( + lastIndex, + endIndex + ); + processStreamFragment(contentToStream); + + const identifierContent = match[1]; + onStreamIdentifier(identifierContent); + + lastIndex = endIndex; + } + + if (lastIndex > 0) { + // Remove all of the processed content. + contentSinceLastIdentifier = contentSinceLastIdentifier.slice(lastIndex); + // Reset the regex. + regex.lastIndex = 0; + } else { + // Clear as much of the content as we can safely. + const maxUnprocessedLength = identifier.start.length - 1; + if (contentSinceLastIdentifier.length > maxUnprocessedLength) { + const identifierIndex = contentSinceLastIdentifier.indexOf( + identifier.start + ); + if (identifierIndex > -1) { + // We have an identifier, so clear up until the identifier. + const contentToStream = contentSinceLastIdentifier.slice( + 0, + identifierIndex + ); + processStreamFragment(contentToStream); + contentSinceLastIdentifier = + contentSinceLastIdentifier.slice(identifierIndex); + } else { + // No identifier, so clear up until the last maxUnprocessedLength. + const processUpTo = + contentSinceLastIdentifier.length - maxUnprocessedLength; + const contentToStream = contentSinceLastIdentifier.slice( + 0, + processUpTo + ); + processStreamFragment(contentToStream); + contentSinceLastIdentifier = + contentSinceLastIdentifier.slice(processUpTo); + } + } + } + } + + // Finish up anything not streamed yet. + if (contentSinceLastIdentifier.length > 0) { + processStreamFragment(contentSinceLastIdentifier); + } +} diff --git a/src/test/ai-accuracy-tests/assertions.ts b/src/test/ai-accuracy-tests/assertions.ts index 31460ef20..304cc68b8 100644 --- a/src/test/ai-accuracy-tests/assertions.ts +++ b/src/test/ai-accuracy-tests/assertions.ts @@ -3,9 +3,11 @@ import util from 'util'; import type { Document } from 'mongodb'; import type { Fixtures } from './fixtures/fixture-loader'; -import { getRunnableContentFromString } from '../../participant/participant'; import { execute } from '../../language/worker'; import type { ShellEvaluateResult } from '../../types/playgroundType'; +import { asyncIterableFromArray } from '../suite/participant/asyncIterableFromArray'; +import { codeBlockIdentifier } from '../../participant/constants'; +import { processStreamWithIdentifiers } from '../../participant/streamParsing'; export const runCodeInMessage = async ( message: string, @@ -15,7 +17,18 @@ export const runCodeInMessage = async ( data: ShellEvaluateResult; error: any; }> => { - const codeToEvaluate = getRunnableContentFromString(message); + // We only run the last code block passed. + let codeToEvaluate = ''; + await processStreamWithIdentifiers({ + processStreamFragment: () => { + /* no-op */ + }, + onStreamIdentifier: (codeBlockContent: string): void => { + codeToEvaluate = codeBlockContent; + }, + inputIterable: asyncIterableFromArray([message]), + identifier: codeBlockIdentifier, + }); if (codeToEvaluate.trim().length === 0) { throw new Error(`no code found in message: ${message}`); diff --git a/src/test/suite/participant/asyncIterableFromArray.ts b/src/test/suite/participant/asyncIterableFromArray.ts new file mode 100644 index 000000000..e3b7c8bde --- /dev/null +++ b/src/test/suite/participant/asyncIterableFromArray.ts @@ -0,0 +1,24 @@ +// Exported here so that the accuracy tests can use it without +// needing to define all of the testing types the main tests have. +export function asyncIterableFromArray(array: T[]): AsyncIterable { + return { + [Symbol.asyncIterator](): { + next(): Promise>; + } { + let index = 0; + return { + next(): Promise<{ + value: any; + done: boolean; + }> { + if (index < array.length) { + const value = array[index++]; + return Promise.resolve({ value, done: false }); + } + + return Promise.resolve({ value: undefined, done: true }); + }, + }; + }, + }; +} diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 557fbd320..2610ccddf 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -6,9 +6,7 @@ import sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; import { ObjectId, Int32 } from 'bson'; -import ParticipantController, { - getRunnableContentFromString, -} from '../../../participant/participant'; +import ParticipantController from '../../../participant/participant'; import ConnectionController from '../../../connectionController'; import { StorageController } from '../../../storage'; import { StatusView } from '../../../views'; @@ -160,18 +158,6 @@ suite('Participant Controller Test Suite', function () { expect(collectionName).to.be.equal('cats'); }); - test('parses a returned by ai text for code blocks', function () { - const text = - '```javascript\n' + - "use('test');\n" + - "db.getCollection('test').find({ name: 'Shika' });\n" + - '```'; - const code = getRunnableContentFromString(text); - expect(code).to.be.equal( - "use('test');\ndb.getCollection('test').find({ name: 'Shika' });" - ); - }); - suite('when not connected', function () { let connectWithConnectionIdStub; let changeActiveConnectionStub; diff --git a/src/test/suite/participant/streamParsing.test.ts b/src/test/suite/participant/streamParsing.test.ts new file mode 100644 index 000000000..66208ecdd --- /dev/null +++ b/src/test/suite/participant/streamParsing.test.ts @@ -0,0 +1,219 @@ +import { beforeEach } from 'mocha'; +import { expect } from 'chai'; + +import { processStreamWithIdentifiers } from '../../../participant/streamParsing'; +import { asyncIterableFromArray } from './asyncIterableFromArray'; + +const defaultCodeBlockIdentifier = { + start: '```', + end: '```', +}; + +suite('processStreamWithIdentifiers', () => { + let fragmentsProcessed: string[] = []; + let identifiersStreamed: string[] = []; + + const processStreamFragment = (fragment: string): void => { + fragmentsProcessed.push(fragment); + }; + + const onStreamIdentifier = (content: string): void => { + identifiersStreamed.push(content); + }; + + beforeEach(function () { + fragmentsProcessed = []; + identifiersStreamed = []; + }); + + test('empty', async () => { + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable: asyncIterableFromArray([]), + identifier: defaultCodeBlockIdentifier, + }); + + expect(fragmentsProcessed).to.be.empty; + expect(identifiersStreamed).to.be.empty; + }); + + test('input with no code block', async () => { + const inputText = 'This is some sample text without code blocks.'; + const inputFragments = inputText.match(/.{1,5}/g) || []; + const inputIterable = asyncIterableFromArray(inputFragments); + + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier: defaultCodeBlockIdentifier, + }); + + expect(fragmentsProcessed.join('')).to.equal(inputText); + expect(identifiersStreamed).to.be.empty; + }); + + test('one code block with fragment sizes 2', async () => { + const inputText = '```javascript\npineapple\n```\nMore text.'; + const inputFragments: string[] = []; + let index = 0; + const fragmentSize = 2; + while (index < inputText.length) { + const fragment = inputText.substr(index, fragmentSize); + inputFragments.push(fragment); + index += fragmentSize; + } + + const inputIterable = asyncIterableFromArray(inputFragments); + + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier: { + start: '```javascript', + end: '```', + }, + }); + + expect(fragmentsProcessed.join('')).to.equal(inputText); + expect(identifiersStreamed).to.have.lengthOf(1); + expect(identifiersStreamed[0]).to.equal('\npineapple\n'); + }); + + test('multiple code blocks', async () => { + const inputText = + 'Text before code.\n```\ncode1\n```\nText between code.\n```\ncode2\n```\nText after code.'; + const inputFragments = inputText.split(''); + + const inputIterable = asyncIterableFromArray(inputFragments); + + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier: defaultCodeBlockIdentifier, + }); + + expect(fragmentsProcessed.join('')).to.equal(inputText); + expect(identifiersStreamed).to.deep.equal(['\ncode1\n', '\ncode2\n']); + }); + + test('unfinished code block', async () => { + const inputText = + 'Text before code.\n```\ncode content without end identifier.'; + const inputFragments = inputText.split(''); + + const inputIterable = asyncIterableFromArray(inputFragments); + + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier: defaultCodeBlockIdentifier, + }); + + expect(fragmentsProcessed.join('')).to.equal(inputText); + expect(identifiersStreamed).to.be.empty; + }); + + test('code block identifier is a fragment', async () => { + const inputFragments = [ + 'Text before code.\n', + '```js', + '\ncode content\n', + '```', + '```js', + '\npineapple\n', + '```', + '\nText after code.', + ]; + + const inputIterable = asyncIterableFromArray(inputFragments); + + const identifier = { start: '```js', end: '```' }; + + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier, + }); + + expect(fragmentsProcessed.join('')).to.deep.equal(inputFragments.join('')); + + expect(identifiersStreamed).to.deep.equal([ + '\ncode content\n', + '\npineapple\n', + ]); + }); + + test('code block identifier split between fragments', async () => { + const inputFragments = [ + 'Text before code.\n`', + '``j', + 's\ncode content\n`', + '``', + '\nText after code.', + ]; + + const inputIterable = asyncIterableFromArray(inputFragments); + + const identifier = { start: '```js', end: '```' }; + + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier, + }); + + expect(fragmentsProcessed.join('')).to.deep.equal(inputFragments.join('')); + + expect(identifiersStreamed).to.deep.equal(['\ncode content\n']); + }); + + test('fragments containing multiple code blocks', async () => { + const inputFragments = [ + 'Text before code.\n```', + 'js\ncode1\n```', + '\nText', + ' between code.\n``', + '`js\ncode2\n``', + '`\nText after code.', + ]; + + const inputIterable = asyncIterableFromArray(inputFragments); + const identifier = { start: '```js', end: '```' }; + + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier, + }); + + expect(fragmentsProcessed.join('')).to.deep.equal(inputFragments.join('')); + expect(identifiersStreamed).to.deep.equal(['\ncode1\n', '\ncode2\n']); + }); + + test('one fragment containing multiple code blocks', async () => { + const inputFragments = [ + 'Text before code.\n```js\ncode1\n```\nText between code.\n```js\ncode2\n```\nText after code.', + ]; + + const inputIterable = asyncIterableFromArray(inputFragments); + const identifier = { start: '```js', end: '```' }; + + await processStreamWithIdentifiers({ + processStreamFragment, + onStreamIdentifier, + inputIterable, + identifier, + }); + + expect(fragmentsProcessed.join('')).to.deep.equal(inputFragments.join('')); + expect(identifiersStreamed).to.deep.equal(['\ncode1\n', '\ncode2\n']); + }); +}); From 927933959e2fe0c78c7c139398762be4d7dd9c7f Mon Sep 17 00:00:00 2001 From: Rhys Date: Fri, 27 Sep 2024 13:48:37 -0400 Subject: [PATCH 38/45] chore: update vsce dep (#840) --- package-lock.json | 78 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ffe1c822..14155662c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vscode/test-electron": "^2.4.1", - "@vscode/vsce": "^2.31.1", + "@vscode/vsce": "^3.1.0", "buffer": "^6.0.3", "chai": "^4.5.0", "chai-as-promised": "^7.1.2", @@ -6525,10 +6525,11 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.31.1", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.31.1.tgz", - "integrity": "sha512-LwEQFKXV21C4/brvGPH/9+7ZOUM5cbK7oJ4fVmy0YG75NIy1HV8eMSoBZrl+u23NxpAhor62Cu1aI+JFtCtjSg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.1.0.tgz", + "integrity": "sha512-fwdfp1Ol+bZtlSGkpcd/nztfo6+SVsTOMWjZ/+a88lVtUn7gXNbSu7dbniecl5mz4vINl+oaVDVtVdGbJDApmw==", "dev": true, + "license": "MIT", "dependencies": { "@azure/identity": "^4.1.0", "@vscode/vsce-sign": "^2.0.0", @@ -6542,7 +6543,7 @@ "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", - "markdown-it": "^12.3.2", + "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^3.0.3", "parse-semver": "^1.1.1", @@ -6559,7 +6560,7 @@ "vsce": "vsce" }, "engines": { - "node": ">= 16" + "node": ">= 20" }, "optionalDependencies": { "keytar": "^7.7.0" @@ -14066,12 +14067,13 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, + "license": "MIT", "dependencies": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, "node_modules/load-json-file": { @@ -14368,35 +14370,29 @@ "dev": true }, "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, "bin": { - "markdown-it": "bin/markdown-it.js" + "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdown-it/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } + "license": "Python-2.0" }, "node_modules/matcher": { "version": "3.0.0", @@ -14441,10 +14437,11 @@ "license": "CC0-1.0" }, "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" }, "node_modules/media-typer": { "version": "0.3.0", @@ -17072,6 +17069,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -19769,10 +19776,11 @@ } }, "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" }, "node_modules/unbox-primitive": { "version": "1.0.2", diff --git a/package.json b/package.json index 65c6f1625..d8c108c3d 100644 --- a/package.json +++ b/package.json @@ -1243,7 +1243,7 @@ "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vscode/test-electron": "^2.4.1", - "@vscode/vsce": "^2.31.1", + "@vscode/vsce": "^3.1.0", "buffer": "^6.0.3", "chai": "^4.5.0", "chai-as-promised": "^7.1.2", From 60021d641c23981ca7e72877770041fe502d2031 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 27 Sep 2024 22:13:12 +0200 Subject: [PATCH 39/45] chore(chat): Remove proposed API (#842) --- package.json | 4 +- src/participant/participant.ts | 11 +- .../suite/participant/participant.test.ts | 2 +- ...ode.proposed.chatParticipantAdditions.d.ts | 468 ------------------ 4 files changed, 12 insertions(+), 473 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts diff --git a/package.json b/package.json index d8c108c3d..a15bde55d 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,7 @@ "color": "#3D4F58", "theme": "dark" }, - "enabledApiProposals": [ - "chatParticipantAdditions" - ], + "enabledApiProposals": [], "license": "SEE LICENSE IN LICENSE.txt", "main": "./dist/extension.js", "scripts": { diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 8d57f0726..ee431a884 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -1417,9 +1417,18 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i if (feedback.result.metadata?.intent === 'docs') { await this._rateDocsChatbotMessage(feedback); } + + // unhelpfulReason is available in insider builds and is accessed through + // https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts + // Since this is a proposed API, we can't depend on it being available, which is why + // we're dynamically checking for it. + const unhelpfulReason = + 'unhelpfulReason' in feedback + ? (feedback.unhelpfulReason as string) + : undefined; this._telemetryService.trackCopilotParticipantFeedback({ feedback: chatResultFeedbackKindToTelemetryValue(feedback.kind), - reason: feedback.unhelpfulReason, + reason: unhelpfulReason, response_type: (feedback.result as ChatResult)?.metadata.intent, }); } diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 2610ccddf..09849239c 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1578,7 +1578,7 @@ Schema: }, }, unhelpfulReason: 'incompleteCode', - }); + } as vscode.ChatResultFeedback); sinon.assert.calledOnce(telemetryTrackStub); expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts deleted file mode 100644 index 315fb76ed..000000000 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ /dev/null @@ -1,468 +0,0 @@ -/* --------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - export interface ChatParticipant { - onDidPerformAction: Event; - } - - /** - * Now only used for the "intent detection" API below - */ - export interface ChatCommand { - readonly name: string; - readonly description: string; - } - - export class ChatResponseDetectedParticipantPart { - participant: string; - // TODO@API validate this against statically-declared slash commands? - command?: ChatCommand; - constructor(participant: string, command?: ChatCommand); - } - - export interface ChatVulnerability { - title: string; - description: string; - // id: string; // Later we will need to be able to link these across multiple content chunks. - } - - export class ChatResponseMarkdownWithVulnerabilitiesPart { - value: MarkdownString; - vulnerabilities: ChatVulnerability[]; - constructor( - value: string | MarkdownString, - vulnerabilities: ChatVulnerability[] - ); - } - - /** - * Displays a {@link Command command} as a button in the chat response. - */ - export interface ChatCommandButton { - command: Command; - } - - export interface ChatDocumentContext { - uri: Uri; - version: number; - ranges: Range[]; - } - - export class ChatResponseTextEditPart { - uri: Uri; - edits: TextEdit[]; - constructor(uri: Uri, edits: TextEdit | TextEdit[]); - } - - export class ChatResponseConfirmationPart { - title: string; - message: string; - data: any; - buttons?: string[]; - constructor(title: string, message: string, data: any, buttons?: string[]); - } - - export class ChatResponseCodeCitationPart { - value: Uri; - license: string; - snippet: string; - constructor(value: Uri, license: string, snippet: string); - } - - export type ExtendedChatResponsePart = - | ChatResponsePart - | ChatResponseTextEditPart - | ChatResponseDetectedParticipantPart - | ChatResponseConfirmationPart - | ChatResponseCodeCitationPart - | ChatResponseReferencePart2 - | ChatResponseMovePart; - - export class ChatResponseWarningPart { - value: MarkdownString; - constructor(value: string | MarkdownString); - } - - export class ChatResponseProgressPart2 extends ChatResponseProgressPart { - value: string; - task?: ( - progress: Progress - ) => Thenable; - constructor( - value: string, - task?: ( - progress: Progress - ) => Thenable - ); - } - - export class ChatResponseReferencePart2 { - /** - * The reference target. - */ - value: - | Uri - | Location - | { variableName: string; value?: Uri | Location } - | string; - - /** - * The icon for the reference. - */ - iconPath?: - | Uri - | ThemeIcon - | { - /** - * The icon path for the light theme. - */ - light: Uri; - /** - * The icon path for the dark theme. - */ - dark: Uri; - }; - options?: { - status?: { - description: string; - kind: ChatResponseReferencePartStatusKind; - }; - }; - - /** - * Create a new ChatResponseReferencePart. - * @param value A uri or location - * @param iconPath Icon for the reference shown in UI - */ - constructor( - value: - | Uri - | Location - | { variableName: string; value?: Uri | Location } - | string, - iconPath?: - | Uri - | ThemeIcon - | { - /** - * The icon path for the light theme. - */ - light: Uri; - /** - * The icon path for the dark theme. - */ - dark: Uri; - }, - options?: { - status?: { - description: string; - kind: ChatResponseReferencePartStatusKind; - }; - } - ); - } - - export class ChatResponseMovePart { - readonly uri: Uri; - readonly range: Range; - - constructor(uri: Uri, range: Range); - } - - // Extended to add `SymbolInformation`. Would also be added to `constructor`. - export interface ChatResponseAnchorPart { - /** - * The target of this anchor. - * - * If this is a {@linkcode Uri} or {@linkcode Location}, this is rendered as a normal link. - * - * If this is a {@linkcode SymbolInformation}, this is rendered as a symbol link. - */ - value2: Uri | Location | SymbolInformation; - } - - export interface ChatResponseStream { - /** - * Push a progress part to this stream. Short-hand for - * `push(new ChatResponseProgressPart(value))`. - * - * @param value A progress message - * @param task If provided, a task to run while the progress is displayed. When the Thenable resolves, the progress will be marked complete in the UI, and the progress message will be updated to the resolved string if one is specified. - * @returns This stream. - */ - progress( - value: string, - task?: ( - progress: Progress - ) => Thenable - ): void; - - textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; - markdownWithVulnerabilities( - value: string | MarkdownString, - vulnerabilities: ChatVulnerability[] - ): void; - detectedParticipant(participant: string, command?: ChatCommand): void; - push( - part: - | ChatResponsePart - | ChatResponseTextEditPart - | ChatResponseDetectedParticipantPart - | ChatResponseWarningPart - | ChatResponseProgressPart2 - ): void; - - /** - * Show an inline message in the chat view asking the user to confirm an action. - * Multiple confirmations may be shown per response. The UI might show "Accept All" / "Reject All" actions. - * @param title The title of the confirmation entry - * @param message An extra message to display to the user - * @param data An arbitrary JSON-stringifiable object that will be included in the ChatRequest when - * the confirmation is accepted or rejected - * TODO@API should this be MarkdownString? - * TODO@API should actually be a more generic function that takes an array of buttons - */ - confirmation( - title: string, - message: string, - data: any, - buttons?: string[] - ): void; - - /** - * Push a warning to this stream. Short-hand for - * `push(new ChatResponseWarningPart(message))`. - * - * @param message A warning message - * @returns This stream. - */ - warning(message: string | MarkdownString): void; - - reference( - value: Uri | Location | { variableName: string; value?: Uri | Location }, - iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri } - ): void; - - reference2( - value: - | Uri - | Location - | string - | { variableName: string; value?: Uri | Location }, - iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }, - options?: { - status?: { - description: string; - kind: ChatResponseReferencePartStatusKind; - }; - } - ): void; - - codeCitation(value: Uri, license: string, snippet: string): void; - - push(part: ExtendedChatResponsePart): void; - } - - export enum ChatResponseReferencePartStatusKind { - Complete = 1, - Partial = 2, - Omitted = 3, - } - - /** - * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? - * Does it show up in history? - */ - export interface ChatRequest { - /** - * The `data` for any confirmations that were accepted - */ - acceptedConfirmationData?: any[]; - - /** - * The `data` for any confirmations that were rejected - */ - rejectedConfirmationData?: any[]; - } - - // TODO@API fit this into the stream - export interface ChatUsedContext { - documents: ChatDocumentContext[]; - } - - export interface ChatParticipant { - /** - * Provide a set of variables that can only be used with this participant. - */ - participantVariableProvider?: { - provider: ChatParticipantCompletionItemProvider; - triggerCharacters: string[]; - }; - } - - export interface ChatParticipantCompletionItemProvider { - provideCompletionItems( - query: string, - token: CancellationToken - ): ProviderResult; - } - - export class ChatCompletionItem { - id: string; - label: string | CompletionItemLabel; - values: ChatVariableValue[]; - fullName?: string; - icon?: ThemeIcon; - insertText?: string; - detail?: string; - documentation?: string | MarkdownString; - command?: Command; - - constructor( - id: string, - label: string | CompletionItemLabel, - values: ChatVariableValue[] - ); - } - - export type ChatExtendedRequestHandler = ( - request: ChatRequest, - context: ChatContext, - response: ChatResponseStream, - token: CancellationToken - ) => ProviderResult; - - export interface ChatResult { - nextQuestion?: { - prompt: string; - participant?: string; - command?: string; - }; - } - - export namespace chat { - /** - * Create a chat participant with the extended progress type - */ - export function createChatParticipant( - id: string, - handler: ChatExtendedRequestHandler - ): ChatParticipant; - - export function registerChatParticipantDetectionProvider( - participantDetectionProvider: ChatParticipantDetectionProvider - ): Disposable; - } - - export interface ChatParticipantMetadata { - participant: string; - command?: string; - disambiguation: { - category: string; - description: string; - examples: string[]; - }[]; - } - - export interface ChatParticipantDetectionResult { - participant: string; - command?: string; - } - - export interface ChatParticipantDetectionProvider { - provideParticipantDetection( - chatRequest: ChatRequest, - context: ChatContext, - options: { - participants?: ChatParticipantMetadata[]; - location: ChatLocation; - }, - token: CancellationToken - ): ProviderResult; - } - - /* - * User action events - */ - - export enum ChatCopyKind { - // Keyboard shortcut or context menu - Action = 1, - Toolbar = 2, - } - - export interface ChatCopyAction { - // eslint-disable-next-line local/vscode-dts-string-type-literals - kind: 'copy'; - codeBlockIndex: number; - copyKind: ChatCopyKind; - copiedCharacters: number; - totalCharacters: number; - copiedText: string; - } - - export interface ChatInsertAction { - // eslint-disable-next-line local/vscode-dts-string-type-literals - kind: 'insert'; - codeBlockIndex: number; - totalCharacters: number; - newFile?: boolean; - userAction?: string; - codeMapper?: string; - } - - export interface ChatTerminalAction { - // eslint-disable-next-line local/vscode-dts-string-type-literals - kind: 'runInTerminal'; - codeBlockIndex: number; - languageId?: string; - } - - export interface ChatCommandAction { - // eslint-disable-next-line local/vscode-dts-string-type-literals - kind: 'command'; - commandButton: ChatCommandButton; - } - - export interface ChatFollowupAction { - // eslint-disable-next-line local/vscode-dts-string-type-literals - kind: 'followUp'; - followup: ChatFollowup; - } - - export interface ChatBugReportAction { - // eslint-disable-next-line local/vscode-dts-string-type-literals - kind: 'bug'; - } - - export interface ChatEditorAction { - kind: 'editor'; - accepted: boolean; - } - - export interface ChatUserActionEvent { - readonly result: ChatResult; - readonly action: - | ChatCopyAction - | ChatInsertAction - | ChatTerminalAction - | ChatCommandAction - | ChatFollowupAction - | ChatBugReportAction - | ChatEditorAction; - } - - export interface ChatPromptReference { - /** - * TODO Needed for now to drive the variableName-type reference, but probably both of these should go away in the future. - */ - readonly name: string; - } - - export interface ChatResultFeedback { - readonly unhelpfulReason?: string; - } -} From 6d7150fd1a1e18a0d16cf4b838cf172b018067f5 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 27 Sep 2024 23:05:10 +0200 Subject: [PATCH 40/45] chore(participant) Wire up telemetry for user prompts VSCODE-606 (#836) * Wire up telemetry for user prompts * Remove .only * Clean up rebase artifacts * Remove .only * Address CR feedback * history_length -> history_size --- src/participant/participant.ts | 49 ++-- src/participant/prompts/intent.ts | 5 + src/participant/prompts/namespace.ts | 5 + src/participant/prompts/promptBase.ts | 55 ++++- src/participant/prompts/query.ts | 20 +- src/participant/prompts/schema.ts | 15 +- src/telemetry/telemetryService.ts | 16 ++ .../ai-accuracy-tests/ai-accuracy-tests.ts | 7 +- .../suite/participant/participant.test.ts | 218 ++++++++++++++++-- 9 files changed, 330 insertions(+), 60 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index ee431a884..a613aadc4 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -39,6 +39,7 @@ import { } from '../telemetry/telemetryService'; import { DocsChatbotAIService } from './docsChatbotAIService'; import type TelemetryService from '../telemetry/telemetryService'; +import type { ModelInput } from './prompts/promptBase'; import { processStreamWithIdentifiers } from './streamParsing'; import type { PromptIntent } from './prompts/intent'; @@ -164,10 +165,10 @@ export default class ParticipantController { } async _getChatResponse({ - messages, + modelInput, token, }: { - messages: vscode.LanguageModelChatMessage[]; + modelInput: ModelInput; token: vscode.CancellationToken; }): Promise { const model = await getCopilotModel(); @@ -176,20 +177,22 @@ export default class ParticipantController { throw new Error('Copilot model not found'); } - return await model.sendRequest(messages, {}, token); + this._telemetryService.trackCopilotParticipantPrompt(modelInput.stats); + + return await model.sendRequest(modelInput.messages, {}, token); } async streamChatResponse({ - messages, + modelInput, stream, token, }: { - messages: vscode.LanguageModelChatMessage[]; + modelInput: ModelInput; stream: vscode.ChatResponseStream; token: vscode.CancellationToken; }): Promise { const chatResponse = await this._getChatResponse({ - messages, + modelInput, token, }); for await (const fragment of chatResponse.text) { @@ -226,16 +229,16 @@ export default class ParticipantController { } async streamChatResponseContentWithCodeActions({ - messages, + modelInput, stream, token, }: { - messages: vscode.LanguageModelChatMessage[]; + modelInput: ModelInput; stream: vscode.ChatResponseStream; token: vscode.CancellationToken; }): Promise { const chatResponse = await this._getChatResponse({ - messages, + modelInput, token, }); @@ -254,15 +257,15 @@ export default class ParticipantController { // This will stream all of the response content and create a string from it. // It should only be used when the entire response is needed at one time. async getChatResponseContent({ - messages, + modelInput, token, }: { - messages: vscode.LanguageModelChatMessage[]; + modelInput: ModelInput; token: vscode.CancellationToken; }): Promise { let responseContent = ''; const chatResponse = await this._getChatResponse({ - messages, + modelInput, token, }); for await (const fragment of chatResponse.text) { @@ -278,14 +281,14 @@ export default class ParticipantController { stream: vscode.ChatResponseStream, token: vscode.CancellationToken ): Promise { - const messages = await Prompts.generic.buildMessages({ + const modelInput = await Prompts.generic.buildMessages({ request, context, connectionNames: this._getConnectionNames(), }); await this.streamChatResponseContentWithCodeActions({ - messages, + modelInput, token, stream, }); @@ -334,14 +337,14 @@ export default class ParticipantController { request: vscode.ChatRequest; token: vscode.CancellationToken; }): Promise { - const messages = await Prompts.intent.buildMessages({ + const modelInput = await Prompts.intent.buildMessages({ connectionNames: this._getConnectionNames(), request, context, }); const responseContent = await this.getChatResponseContent({ - messages, + modelInput, token, }); @@ -708,7 +711,7 @@ export default class ParticipantController { connectionNames: this._getConnectionNames(), }); const responseContentWithNamespace = await this.getChatResponseContent({ - messages: messagesWithNamespace, + modelInput: messagesWithNamespace, token, }); const { databaseName, collectionName } = @@ -1043,7 +1046,7 @@ export default class ParticipantController { return schemaRequestChatResult(context.history); } - const messages = await Prompts.schema.buildMessages({ + const modelInput = await Prompts.schema.buildMessages({ request, context, databaseName, @@ -1054,7 +1057,7 @@ export default class ParticipantController { ...(sampleDocuments ? { sampleDocuments } : {}), }); await this.streamChatResponse({ - messages, + modelInput, stream, token, }); @@ -1147,7 +1150,7 @@ export default class ParticipantController { ); } - const messages = await Prompts.query.buildMessages({ + const modelInput = await Prompts.query.buildMessages({ request, context, databaseName, @@ -1158,7 +1161,7 @@ export default class ParticipantController { }); await this.streamChatResponseContentWithCodeActions({ - messages, + modelInput, stream, token, }); @@ -1230,14 +1233,14 @@ export default class ParticipantController { ] ): Promise { const [request, context, stream, token] = args; - const messages = await Prompts.generic.buildMessages({ + const modelInput = await Prompts.generic.buildMessages({ request, context, connectionNames: this._getConnectionNames(), }); await this.streamChatResponseContentWithCodeActions({ - messages, + modelInput, stream, token, }); diff --git a/src/participant/prompts/intent.ts b/src/participant/prompts/intent.ts index 4d6216afa..8a1266f69 100644 --- a/src/participant/prompts/intent.ts +++ b/src/participant/prompts/intent.ts @@ -1,3 +1,4 @@ +import type { InternalPromptPurpose } from '../../telemetry/telemetryService'; import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; @@ -47,4 +48,8 @@ Docs`; return 'Default'; } } + + protected get internalPurposeForTelemetry(): InternalPromptPurpose { + return 'intent'; + } } diff --git a/src/participant/prompts/namespace.ts b/src/participant/prompts/namespace.ts index e29f24d2c..c5428f191 100644 --- a/src/participant/prompts/namespace.ts +++ b/src/participant/prompts/namespace.ts @@ -1,3 +1,4 @@ +import type { InternalPromptPurpose } from '../../telemetry/telemetryService'; import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; @@ -50,4 +51,8 @@ No names found. const collectionName = text.match(COL_NAME_REGEX)?.[1].trim(); return { databaseName, collectionName }; } + + protected get internalPurposeForTelemetry(): InternalPromptPurpose { + return 'namespace'; + } } diff --git a/src/participant/prompts/promptBase.ts b/src/participant/prompts/promptBase.ts index 0f3c83286..949b4f3d0 100644 --- a/src/participant/prompts/promptBase.ts +++ b/src/participant/prompts/promptBase.ts @@ -1,5 +1,9 @@ import * as vscode from 'vscode'; import type { ChatResult, ParticipantResponseType } from '../constants'; +import type { + InternalPromptPurpose, + ParticipantPromptProperties, +} from '../../telemetry/telemetryService'; export interface PromptArgsBase { request: { @@ -10,14 +14,31 @@ export interface PromptArgsBase { connectionNames: string[]; } +export interface UserPromptResponse { + prompt: string; + hasSampleDocs: boolean; +} + +export interface ModelInput { + messages: vscode.LanguageModelChatMessage[]; + stats: ParticipantPromptProperties; +} + export abstract class PromptBase { protected abstract getAssistantPrompt(args: TArgs): string; - protected getUserPrompt(args: TArgs): Promise { - return Promise.resolve(args.request.prompt); + protected get internalPurposeForTelemetry(): InternalPromptPurpose { + return undefined; } - async buildMessages(args: TArgs): Promise { + protected getUserPrompt(args: TArgs): Promise { + return Promise.resolve({ + prompt: args.request.prompt, + hasSampleDocs: false, + }); + } + + async buildMessages(args: TArgs): Promise { let historyMessages = this.getHistoryMessages(args); // If the current user's prompt is a connection name, and the last // message was to connect. We want to use the last @@ -49,13 +70,37 @@ export abstract class PromptBase { } } - return [ + const { prompt, hasSampleDocs } = await this.getUserPrompt(args); + const messages = [ // eslint-disable-next-line new-cap vscode.LanguageModelChatMessage.Assistant(this.getAssistantPrompt(args)), ...historyMessages, // eslint-disable-next-line new-cap - vscode.LanguageModelChatMessage.User(await this.getUserPrompt(args)), + vscode.LanguageModelChatMessage.User(prompt), ]; + + return { + messages, + stats: this.getStats(messages, args, hasSampleDocs), + }; + } + + protected getStats( + messages: vscode.LanguageModelChatMessage[], + { request, context }: TArgs, + hasSampleDocs: boolean + ): ParticipantPromptProperties { + return { + total_message_length: messages.reduce( + (acc, message) => acc + message.content.length, + 0 + ), + user_input_length: request.prompt.length, + has_sample_documents: hasSampleDocs, + command: request.command || 'generic', + history_size: context.history.length, + internal_purpose: this.internalPurposeForTelemetry, + }; } // When passing the history to the model we only want contextual messages diff --git a/src/participant/prompts/query.ts b/src/participant/prompts/query.ts index eff4d29ff..1efef4ba6 100644 --- a/src/participant/prompts/query.ts +++ b/src/participant/prompts/query.ts @@ -2,8 +2,8 @@ import * as vscode from 'vscode'; import type { Document } from 'bson'; import { getStringifiedSampleDocuments } from '../sampleDocuments'; +import type { PromptArgsBase, UserPromptResponse } from './promptBase'; import { codeBlockIdentifier } from '../constants'; -import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; interface QueryPromptArgs extends PromptArgsBase { @@ -59,21 +59,23 @@ db.getCollection('');\n`; request, schema, sampleDocuments, - }: QueryPromptArgs): Promise { + }: QueryPromptArgs): Promise { let prompt = request.prompt; prompt += `\nDatabase name: ${databaseName}\n`; prompt += `Collection name: ${collectionName}\n`; if (schema) { prompt += `Collection schema: ${schema}\n`; } - if (sampleDocuments) { - prompt += await getStringifiedSampleDocuments({ - sampleDocuments, - prompt, - }); - } - return prompt; + const sampleDocumentsPrompt = await getStringifiedSampleDocuments({ + sampleDocuments, + prompt, + }); + + return { + prompt: `${prompt}${sampleDocumentsPrompt}`, + hasSampleDocs: !!sampleDocumentsPrompt, + }; } get emptyRequestResponse(): string { diff --git a/src/participant/prompts/schema.ts b/src/participant/prompts/schema.ts index 895f99568..ca8b54b26 100644 --- a/src/participant/prompts/schema.ts +++ b/src/participant/prompts/schema.ts @@ -1,3 +1,4 @@ +import type { UserPromptResponse } from './promptBase'; import { PromptBase, type PromptArgsBase } from './promptBase'; export const DOCUMENTS_TO_SAMPLE_FOR_SCHEMA_PROMPT = 100; @@ -11,7 +12,6 @@ export interface SchemaPromptArgs extends PromptArgsBase { collectionName: string; schema: string; amountOfDocumentsSampled: number; - connectionNames: string[]; } export class SchemaPrompt extends PromptBase { @@ -30,13 +30,16 @@ Amount of documents sampled: ${amountOfDocumentsSampled}.`; collectionName, request, schema, - }: SchemaPromptArgs): Promise { + }: SchemaPromptArgs): Promise { const prompt = request.prompt; - return Promise.resolve(`${ - prompt ? `The user provided additional information: "${prompt}"\n` : '' - }Database name: ${databaseName} + return Promise.resolve({ + prompt: `${ + prompt ? `The user provided additional information: "${prompt}"\n` : '' + }Database name: ${databaseName} Collection name: ${collectionName} Schema: -${schema}`); +${schema}`, + hasSampleDocs: false, + }); } } diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 53dd2cba5..930f7e950 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -108,6 +108,17 @@ type ParticipantResponseFailedProperties = { error_name: ParticipantErrorTypes; }; +export type InternalPromptPurpose = 'intent' | 'namespace' | undefined; + +export type ParticipantPromptProperties = { + command: string; + user_input_length: number; + total_message_length: number; + has_sample_documents: boolean; + history_size: number; + internal_purpose: InternalPromptPurpose; +}; + export function chatResultFeedbackKindToTelemetryValue( kind: vscode.ChatResultFeedbackKind ): TelemetryFeedbackKind { @@ -160,6 +171,7 @@ export enum TelemetryEventTypes { PARTICIPANT_FEEDBACK = 'Participant Feedback', PARTICIPANT_WELCOME_SHOWN = 'Participant Welcome Shown', PARTICIPANT_RESPONSE_FAILED = 'Participant Response Failed', + PARTICIPANT_PROMPT_SUBMITTED = 'Participant Prompt Submitted', } export enum ParticipantErrorTypes { @@ -422,4 +434,8 @@ export default class TelemetryService { trackCopilotParticipantFeedback(props: ParticipantFeedbackProperties): void { this.track(TelemetryEventTypes.PARTICIPANT_FEEDBACK, props); } + + trackCopilotParticipantPrompt(stats: ParticipantPromptProperties): void { + this.track(TelemetryEventTypes.PARTICIPANT_PROMPT_SUBMITTED, stats); + } } diff --git a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts index c3135400d..1b3d45636 100644 --- a/src/test/ai-accuracy-tests/ai-accuracy-tests.ts +++ b/src/test/ai-accuracy-tests/ai-accuracy-tests.ts @@ -19,6 +19,7 @@ import { } from './create-test-results-html-page'; import { anyOf, runCodeInMessage } from './assertions'; import { Prompts } from '../../participant/prompts'; +import type { ModelInput } from '../../participant/prompts/promptBase'; const numberOfRunsPerTest = 1; @@ -489,7 +490,7 @@ const buildMessages = async ({ }: { testCase: TestCase; fixtures: Fixtures; -}): Promise => { +}): Promise => { switch (testCase.type) { case 'intent': return Prompts.intent.buildMessages({ @@ -499,7 +500,7 @@ const buildMessages = async ({ }); case 'generic': - return Prompts.generic.buildMessages({ + return await Prompts.generic.buildMessages({ request: { prompt: testCase.userInput }, context: { history: [] }, connectionNames: [], @@ -552,7 +553,7 @@ async function runTest({ aiBackend: AIBackend; fixtures: Fixtures; }): Promise { - const messages = await buildMessages({ + const { messages } = await buildMessages({ testCase, fixtures, }); diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 09849239c..adde458d8 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -11,6 +11,10 @@ import ConnectionController from '../../../connectionController'; import { StorageController } from '../../../storage'; import { StatusView } from '../../../views'; import { ExtensionContextStub } from '../stubs'; +import type { + InternalPromptPurpose, + ParticipantPromptProperties, +} from '../../../telemetry/telemetryService'; import TelemetryService, { TelemetryEventTypes, } from '../../../telemetry/telemetryService'; @@ -77,6 +81,45 @@ suite('Participant Controller Test Suite', function () { chatTokenStub ); + const assertCommandTelemetry = ( + command: string, + chatRequest: vscode.ChatRequest, + { + expectSampleDocs = false, + callIndex = 0, + expectedCallCount, + expectedInternalPurpose = undefined, + }: { + expectSampleDocs?: boolean; + callIndex: number; + expectedCallCount: number; + expectedInternalPurpose?: InternalPromptPurpose; + } + ): void => { + expect(telemetryTrackStub.callCount).to.equal(expectedCallCount); + + const call = telemetryTrackStub.getCalls()[callIndex]; + expect(call.args[0]).to.equal('Participant Prompt Submitted'); + + const properties = call.args[1] as ParticipantPromptProperties; + + expect(properties.command).to.equal(command); + expect(properties.has_sample_documents).to.equal(expectSampleDocs); + expect(properties.history_size).to.equal(chatContextStub.history.length); + + // Total message length includes participant as well as user prompt + expect(properties.total_message_length).to.be.greaterThan( + properties.user_input_length + ); + + // User prompt length should be at least equal to the supplied user prompt, but my occasionally + // be greater - e.g. when we enhance the context. + expect(properties.user_input_length).to.be.greaterThanOrEqual( + chatRequest.prompt.length + ); + expect(properties.internal_purpose).to.equal(expectedInternalPurpose); + }; + beforeEach(function () { testStorageController = new StorageController(extensionContextStub); testStatusView = new StatusView(extensionContextStub); @@ -382,11 +425,17 @@ suite('Participant Controller Test Suite', function () { const welcomeMessage = chatStreamStub.markdown.firstCall.args[0]; expect(welcomeMessage).to.include('Welcome to MongoDB Participant!'); - sinon.assert.calledOnce(telemetryTrackStub); - expect(telemetryTrackStub.lastCall.args[0]).to.equal( + // Once to report welcome screen shown, second time to track the user prompt + expect(telemetryTrackStub).to.have.been.calledTwice; + expect(telemetryTrackStub.firstCall.args[0]).to.equal( TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN ); - expect(telemetryTrackStub.lastCall.args[1]).to.be.undefined; + expect(telemetryTrackStub.firstCall.args[1]).to.be.undefined; + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 1, + expectedCallCount: 2, + expectedInternalPurpose: 'namespace', + }); }); }); @@ -498,6 +547,17 @@ suite('Participant Controller Test Suite', function () { }, ], }); + + assertCommandTelemetry('generic', chatRequestMock, { + expectedCallCount: 2, + callIndex: 0, + expectedInternalPurpose: 'intent', + }); + + assertCommandTelemetry('generic', chatRequestMock, { + expectedCallCount: 2, + callIndex: 1, + }); }); }); @@ -526,6 +586,17 @@ suite('Participant Controller Test Suite', function () { }, ], }); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 0, + expectedCallCount: 2, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 1, + expectedCallCount: 2, + }); }); test('includes a collection schema', async function () { @@ -551,6 +622,17 @@ suite('Participant Controller Test Suite', function () { 'field.stringField: String\n' + 'field.arrayField: Array\n' ); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 0, + expectedCallCount: 2, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 1, + expectedCallCount: 2, + }); }); suite('useSampleDocsInCopilot setting is true', function () { @@ -617,6 +699,18 @@ suite('Participant Controller Test Suite', function () { ' }\n' + ']\n' ); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 0, + expectedCallCount: 2, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('query', chatRequestMock, { + expectSampleDocs: true, + callIndex: 1, + expectedCallCount: 2, + }); }); test('includes 1 sample document as an object', async function () { @@ -661,6 +755,18 @@ suite('Participant Controller Test Suite', function () { ' }\n' + '}\n' ); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 0, + expectedCallCount: 2, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('query', chatRequestMock, { + expectSampleDocs: true, + callIndex: 1, + expectedCallCount: 2, + }); }); test('includes 1 sample documents when 3 make prompt too long', async function () { @@ -703,6 +809,18 @@ suite('Participant Controller Test Suite', function () { ' }\n' + '}\n' ); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 0, + expectedCallCount: 2, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('query', chatRequestMock, { + expectSampleDocs: true, + callIndex: 1, + expectedCallCount: 2, + }); }); test('does not include sample documents when even 1 makes prompt too long', async function () { @@ -740,6 +858,17 @@ suite('Participant Controller Test Suite', function () { await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; expect(messages[1].content).to.not.include('Sample documents'); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 0, + expectedCallCount: 2, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 1, + expectedCallCount: 2, + }); }); }); @@ -753,6 +882,17 @@ suite('Participant Controller Test Suite', function () { await invokeChatHandler(chatRequestMock); const messages = sendRequestStub.secondCall.args[0]; expect(messages[1].content).to.not.include('Sample documents'); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 0, + expectedCallCount: 2, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('query', chatRequestMock, { + callIndex: 1, + expectedCallCount: 2, + }); }); }); }); @@ -1314,12 +1454,12 @@ Schema: expect(sendRequestStub).to.have.been.called; // Expect the error to be reported through the telemetry service - sinon.assert.calledOnce(telemetryTrackStub); - expect(telemetryTrackStub.lastCall.args[0]).to.equal( + expect(telemetryTrackStub).to.have.been.calledTwice; + expect(telemetryTrackStub.firstCall.args[0]).to.equal( TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED ); - const properties = telemetryTrackStub.lastCall.args[1]; + const properties = telemetryTrackStub.firstCall.args[1]; expect(properties.command).to.equal('docs'); expect(properties.error_name).to.equal('Docs Chatbot API Issue'); }); @@ -1332,7 +1472,7 @@ Schema: const chatRequestMock = { prompt: 'find all docs by a name example', }; - const messages = await Prompts.generic.buildMessages({ + const { messages, stats } = await Prompts.generic.buildMessages({ context: chatContextStub, request: chatRequestMock, connectionNames: [], @@ -1345,6 +1485,13 @@ Schema: expect(messages[1].role).to.equal( vscode.LanguageModelChatMessageRole.User ); + + expect(stats.command).to.equal('generic'); + expect(stats.has_sample_documents).to.be.false; + expect(stats.user_input_length).to.equal(chatRequestMock.prompt.length); + expect(stats.total_message_length).to.equal( + messages[0].content.length + messages[1].content.length + ); }); test('query', async function () { @@ -1364,7 +1511,7 @@ Schema: }), ], }; - const messages = await Prompts.query.buildMessages({ + const { messages, stats } = await Prompts.query.buildMessages({ context: chatContextStub, request: chatRequestMock, collectionName: 'people', @@ -1407,6 +1554,21 @@ Schema: expect(messages[2].role).to.equal( vscode.LanguageModelChatMessageRole.User ); + + expect(stats.command).to.equal('query'); + expect(stats.has_sample_documents).to.be.true; + expect(stats.user_input_length).to.equal(chatRequestMock.prompt.length); + expect(stats.total_message_length).to.equal( + messages[0].content.length + + messages[1].content.length + + messages[2].content.length + ); + + // The length of the user prompt length should be taken from the prompt supplied + // by the user, even if we enhance it with sample docs and schema. + expect(stats.user_input_length).to.be.lessThan( + messages[2].content.length + ); }); test('schema', async function () { @@ -1423,7 +1585,7 @@ Schema: name: String } `; - const messages = await Prompts.schema.buildMessages({ + const { messages, stats } = await Prompts.schema.buildMessages({ context: chatContextStub, request: chatRequestMock, amountOfDocumentsSampled: 3, @@ -1445,6 +1607,13 @@ Schema: expect(messages[1].content).to.include(databaseName); expect(messages[1].content).to.include(collectionName); expect(messages[1].content).to.include(schema); + + expect(stats.command).to.equal('schema'); + expect(stats.has_sample_documents).to.be.false; + expect(stats.user_input_length).to.equal(chatRequestMock.prompt.length); + expect(stats.total_message_length).to.equal( + messages[0].content.length + messages[1].content.length + ); }); test('namespace', async function () { @@ -1452,7 +1621,7 @@ Schema: prompt: 'find all docs by a name example', command: 'query', }; - const messages = await Prompts.namespace.buildMessages({ + const { messages, stats } = await Prompts.namespace.buildMessages({ context: chatContextStub, request: chatRequestMock, connectionNames: [], @@ -1465,6 +1634,13 @@ Schema: expect(messages[1].role).to.equal( vscode.LanguageModelChatMessageRole.User ); + + expect(stats.command).to.equal('query'); + expect(stats.has_sample_documents).to.be.false; + expect(stats.user_input_length).to.equal(chatRequestMock.prompt.length); + expect(stats.total_message_length).to.equal( + messages[0].content.length + messages[1].content.length + ); }); test('removes askForConnect messages from history', async function () { @@ -1475,10 +1651,14 @@ Schema: command: 'query', }; + // This is the prompt of the user prior to us asking them to connect + const expectedPrompt = + 'give me the count of all people in the prod database'; + chatContextStub = { history: [ Object.assign(Object.create(vscode.ChatRequestTurn.prototype), { - prompt: 'give me the count of all people in the prod database', + prompt: expectedPrompt, command: 'query', references: [], participant: CHAT_PARTICIPANT_ID, @@ -1514,7 +1694,7 @@ Schema: ], }; - const messages = await Prompts.query.buildMessages({ + const { messages, stats } = await Prompts.query.buildMessages({ context: chatContextStub, request: chatRequestMock, collectionName: 'people', @@ -1534,8 +1714,18 @@ Schema: expect(messages[1].role).to.equal( vscode.LanguageModelChatMessageRole.User ); - expect(messages[1].content).to.contain( - 'give me the count of all people in the prod database' + expect(messages[1].content).to.contain(expectedPrompt); + + expect(stats.command).to.equal('query'); + expect(stats.has_sample_documents).to.be.false; + expect(stats.user_input_length).to.equal(expectedPrompt.length); + expect(stats.total_message_length).to.equal( + messages[0].content.length + messages[1].content.length + ); + + // The prompt builder may add extra info, but we're only reporting the actual user input + expect(stats.user_input_length).to.be.lessThan( + messages[1].content.length ); }); }); From 13367edb8f627da81aceb9434e8d2d510eb4d23d Mon Sep 17 00:00:00 2001 From: Rhys Date: Fri, 27 Sep 2024 17:18:06 -0400 Subject: [PATCH 41/45] chore: add chat and ai categories to package.json VSCODE-627 (#846) --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index a15bde55d..f4f9bfd43 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ }, "publisher": "mongodb", "categories": [ + "AI", + "Chat", + "Data Science", "Programming Languages", "Snippets", "Other" From 36a6784326b61bc0ec552b3ba653ec6a9f314d6e Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 27 Sep 2024 23:51:13 +0200 Subject: [PATCH 42/45] chore(chat): Report response telemetry VSCODE-603 (#845) --- src/participant/participant.ts | 95 +++++++++-- src/telemetry/telemetryService.ts | 17 +- .../suite/participant/participant.test.ts | 147 +++++++++++++++--- 3 files changed, 220 insertions(+), 39 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index a613aadc4..cbc574b91 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -190,14 +190,21 @@ export default class ParticipantController { modelInput: ModelInput; stream: vscode.ChatResponseStream; token: vscode.CancellationToken; - }): Promise { + }): Promise<{ outputLength: number }> { const chatResponse = await this._getChatResponse({ modelInput, token, }); + + let length = 0; for await (const fragment of chatResponse.text) { stream.markdown(fragment); + length += fragment.length; } + + return { + outputLength: length, + }; } _streamCodeBlockActions({ @@ -236,22 +243,34 @@ export default class ParticipantController { modelInput: ModelInput; stream: vscode.ChatResponseStream; token: vscode.CancellationToken; - }): Promise { + }): Promise<{ + outputLength: number; + hasCodeBlock: boolean; + }> { const chatResponse = await this._getChatResponse({ modelInput, token, }); + let outputLength = 0; + let hasCodeBlock = false; await processStreamWithIdentifiers({ processStreamFragment: (fragment: string) => { stream.markdown(fragment); + outputLength += fragment.length; }, onStreamIdentifier: (content: string) => { this._streamCodeBlockActions({ runnableContent: content, stream }); + hasCodeBlock = true; }, inputIterable: chatResponse.text, identifier: codeBlockIdentifier, }); + + return { + outputLength, + hasCodeBlock, + }; } // This will stream all of the response content and create a string from it. @@ -287,10 +306,19 @@ export default class ParticipantController { connectionNames: this._getConnectionNames(), }); - await this.streamChatResponseContentWithCodeActions({ - modelInput, - token, - stream, + const { hasCodeBlock, outputLength } = + await this.streamChatResponseContentWithCodeActions({ + modelInput, + token, + stream, + }); + + this._telemetryService.trackCopilotParticipantResponse({ + command: 'generic', + has_cta: false, + found_namespace: false, + has_runnable_content: hasCodeBlock, + output_length: outputLength, }); return genericRequestChatResult(context.history); @@ -995,6 +1023,7 @@ export default class ParticipantController { context, token, }); + if (!databaseName || !collectionName) { return await this._askForNamespace({ command: '/schema', @@ -1056,7 +1085,7 @@ export default class ParticipantController { connectionNames: this._getConnectionNames(), ...(sampleDocuments ? { sampleDocuments } : {}), }); - await this.streamChatResponse({ + const response = await this.streamChatResponse({ modelInput, stream, token, @@ -1072,6 +1101,14 @@ export default class ParticipantController { ], }); + this._telemetryService.trackCopilotParticipantResponse({ + command: 'schema', + has_cta: true, + found_namespace: true, + has_runnable_content: false, + output_length: response.outputLength, + }); + return schemaRequestChatResult(context.history); } @@ -1160,10 +1197,19 @@ export default class ParticipantController { ...(sampleDocuments ? { sampleDocuments } : {}), }); - await this.streamChatResponseContentWithCodeActions({ - modelInput, - stream, - token, + const { hasCodeBlock, outputLength } = + await this.streamChatResponseContentWithCodeActions({ + modelInput, + stream, + token, + }); + + this._telemetryService.trackCopilotParticipantResponse({ + command: 'query', + has_cta: false, + found_namespace: true, + has_runnable_content: hasCodeBlock, + output_length: outputLength, }); return queryRequestChatResult(context.history); @@ -1239,11 +1285,12 @@ export default class ParticipantController { connectionNames: this._getConnectionNames(), }); - await this.streamChatResponseContentWithCodeActions({ - modelInput, - stream, - token, - }); + const { hasCodeBlock, outputLength } = + await this.streamChatResponseContentWithCodeActions({ + modelInput, + stream, + token, + }); this._streamResponseReference({ reference: { @@ -1252,6 +1299,14 @@ export default class ParticipantController { }, stream, }); + + this._telemetryService.trackCopilotParticipantResponse({ + command: 'docs/copilot', + has_cta: true, + found_namespace: false, + has_runnable_content: hasCodeBlock, + output_length: outputLength, + }); } _streamResponseReference({ @@ -1307,6 +1362,14 @@ export default class ParticipantController { if (docsResult.responseContent) { stream.markdown(docsResult.responseContent); } + + this._telemetryService.trackCopilotParticipantResponse({ + command: 'docs/chatbot', + has_cta: !!docsResult.responseReferences, + found_namespace: false, + has_runnable_content: false, + output_length: docsResult.responseContent?.length ?? 0, + }); } catch (error) { // If the docs chatbot API is not available, fall back to Copilot’s LLM and include // the MongoDB documentation link for users to go to our documentation site directly. diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 930f7e950..93220661e 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -119,6 +119,14 @@ export type ParticipantPromptProperties = { internal_purpose: InternalPromptPurpose; }; +export type ParticipantResponseProperties = { + command: string; + has_cta: boolean; + has_runnable_content: boolean; + found_namespace: boolean; + output_length: number; +}; + export function chatResultFeedbackKindToTelemetryValue( kind: vscode.ChatResultFeedbackKind ): TelemetryFeedbackKind { @@ -148,7 +156,9 @@ type TelemetryEventProperties = | SavedConnectionsLoadedProperties | SurveyActionProperties | ParticipantFeedbackProperties - | ParticipantResponseFailedProperties; + | ParticipantResponseFailedProperties + | ParticipantPromptProperties + | ParticipantResponseProperties; export enum TelemetryEventTypes { PLAYGROUND_CODE_EXECUTED = 'Playground Code Executed', @@ -172,6 +182,7 @@ export enum TelemetryEventTypes { PARTICIPANT_WELCOME_SHOWN = 'Participant Welcome Shown', PARTICIPANT_RESPONSE_FAILED = 'Participant Response Failed', PARTICIPANT_PROMPT_SUBMITTED = 'Participant Prompt Submitted', + PARTICIPANT_RESPONSE_GENERATED = 'Participant Response Generated', } export enum ParticipantErrorTypes { @@ -438,4 +449,8 @@ export default class TelemetryService { trackCopilotParticipantPrompt(stats: ParticipantPromptProperties): void { this.track(TelemetryEventTypes.PARTICIPANT_PROMPT_SUBMITTED, stats); } + + trackCopilotParticipantResponse(props: ParticipantResponseProperties): void { + this.track(TelemetryEventTypes.PARTICIPANT_RESPONSE_GENERATED, props); + } } diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index adde458d8..b46f19294 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -14,6 +14,7 @@ import { ExtensionContextStub } from '../stubs'; import type { InternalPromptPurpose, ParticipantPromptProperties, + ParticipantResponseProperties, } from '../../../telemetry/telemetryService'; import TelemetryService, { TelemetryEventTypes, @@ -87,16 +88,14 @@ suite('Participant Controller Test Suite', function () { { expectSampleDocs = false, callIndex = 0, - expectedCallCount, expectedInternalPurpose = undefined, }: { expectSampleDocs?: boolean; callIndex: number; - expectedCallCount: number; expectedInternalPurpose?: InternalPromptPurpose; } ): void => { - expect(telemetryTrackStub.callCount).to.equal(expectedCallCount); + expect(telemetryTrackStub.callCount).to.be.greaterThan(callIndex); const call = telemetryTrackStub.getCalls()[callIndex]; expect(call.args[0]).to.equal('Participant Prompt Submitted'); @@ -120,6 +119,33 @@ suite('Participant Controller Test Suite', function () { expect(properties.internal_purpose).to.equal(expectedInternalPurpose); }; + const assertResponseTelemetry = ( + command: string, + { + callIndex = 0, + hasCTA = false, + hasRunnableContent = false, + foundNamespace = false, + }: { + callIndex: number; + hasCTA?: boolean; + hasRunnableContent?: boolean; + foundNamespace?: boolean; + } + ): void => { + expect(telemetryTrackStub.callCount).to.be.greaterThan(callIndex); + const call = telemetryTrackStub.getCalls()[callIndex]; + expect(call.args[0]).to.equal('Participant Response Generated'); + + const properties = call.args[1] as ParticipantResponseProperties; + + expect(properties.command).to.equal(command); + expect(properties.found_namespace).to.equal(foundNamespace); + expect(properties.has_cta).to.equal(hasCTA); + expect(properties.has_runnable_content).to.equal(hasRunnableContent); + expect(properties.output_length).to.be.greaterThan(0); + }; + beforeEach(function () { testStorageController = new StorageController(extensionContextStub); testStatusView = new StatusView(extensionContextStub); @@ -433,7 +459,6 @@ suite('Participant Controller Test Suite', function () { expect(telemetryTrackStub.firstCall.args[1]).to.be.undefined; assertCommandTelemetry('query', chatRequestMock, { callIndex: 1, - expectedCallCount: 2, expectedInternalPurpose: 'namespace', }); }); @@ -549,15 +574,18 @@ suite('Participant Controller Test Suite', function () { }); assertCommandTelemetry('generic', chatRequestMock, { - expectedCallCount: 2, callIndex: 0, expectedInternalPurpose: 'intent', }); assertCommandTelemetry('generic', chatRequestMock, { - expectedCallCount: 2, callIndex: 1, }); + + assertResponseTelemetry('generic', { + callIndex: 2, + hasRunnableContent: true, + }); }); }); @@ -589,13 +617,17 @@ suite('Participant Controller Test Suite', function () { assertCommandTelemetry('query', chatRequestMock, { callIndex: 0, - expectedCallCount: 2, expectedInternalPurpose: 'namespace', }); assertCommandTelemetry('query', chatRequestMock, { callIndex: 1, - expectedCallCount: 2, + }); + + assertResponseTelemetry('query', { + callIndex: 2, + hasRunnableContent: true, + foundNamespace: true, }); }); @@ -625,13 +657,17 @@ suite('Participant Controller Test Suite', function () { assertCommandTelemetry('query', chatRequestMock, { callIndex: 0, - expectedCallCount: 2, expectedInternalPurpose: 'namespace', }); assertCommandTelemetry('query', chatRequestMock, { callIndex: 1, - expectedCallCount: 2, + }); + + assertResponseTelemetry('query', { + callIndex: 2, + hasRunnableContent: true, + foundNamespace: true, }); }); @@ -702,14 +738,18 @@ suite('Participant Controller Test Suite', function () { assertCommandTelemetry('query', chatRequestMock, { callIndex: 0, - expectedCallCount: 2, expectedInternalPurpose: 'namespace', }); assertCommandTelemetry('query', chatRequestMock, { expectSampleDocs: true, callIndex: 1, - expectedCallCount: 2, + }); + + assertResponseTelemetry('query', { + callIndex: 2, + hasRunnableContent: true, + foundNamespace: true, }); }); @@ -758,14 +798,18 @@ suite('Participant Controller Test Suite', function () { assertCommandTelemetry('query', chatRequestMock, { callIndex: 0, - expectedCallCount: 2, expectedInternalPurpose: 'namespace', }); assertCommandTelemetry('query', chatRequestMock, { expectSampleDocs: true, callIndex: 1, - expectedCallCount: 2, + }); + + assertResponseTelemetry('query', { + callIndex: 2, + hasRunnableContent: true, + foundNamespace: true, }); }); @@ -812,14 +856,18 @@ suite('Participant Controller Test Suite', function () { assertCommandTelemetry('query', chatRequestMock, { callIndex: 0, - expectedCallCount: 2, expectedInternalPurpose: 'namespace', }); assertCommandTelemetry('query', chatRequestMock, { expectSampleDocs: true, callIndex: 1, - expectedCallCount: 2, + }); + + assertResponseTelemetry('query', { + callIndex: 2, + hasRunnableContent: true, + foundNamespace: true, }); }); @@ -861,13 +909,17 @@ suite('Participant Controller Test Suite', function () { assertCommandTelemetry('query', chatRequestMock, { callIndex: 0, - expectedCallCount: 2, expectedInternalPurpose: 'namespace', }); assertCommandTelemetry('query', chatRequestMock, { callIndex: 1, - expectedCallCount: 2, + }); + + assertResponseTelemetry('query', { + callIndex: 2, + hasRunnableContent: true, + foundNamespace: true, }); }); }); @@ -885,13 +937,17 @@ suite('Participant Controller Test Suite', function () { assertCommandTelemetry('query', chatRequestMock, { callIndex: 0, - expectedCallCount: 2, expectedInternalPurpose: 'namespace', }); assertCommandTelemetry('query', chatRequestMock, { callIndex: 1, - expectedCallCount: 2, + }); + + assertResponseTelemetry('query', { + callIndex: 2, + hasRunnableContent: true, + foundNamespace: true, }); }); }); @@ -1341,6 +1397,21 @@ suite('Participant Controller Test Suite', function () { }, ], }); + + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 0, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 1, + }); + + assertResponseTelemetry('schema', { + callIndex: 2, + hasCTA: true, + foundNamespace: true, + }); }); test("includes the collection's schema in the request", async function () { @@ -1384,6 +1455,21 @@ Schema: "field", "arrayField" ],`); + + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 0, + expectedInternalPurpose: 'namespace', + }); + + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 1, + }); + + assertResponseTelemetry('schema', { + callIndex: 2, + hasCTA: true, + foundNamespace: true, + }); }); test('prints a message when no documents are found', async function () { @@ -1397,6 +1483,11 @@ Schema: expect(chatStreamStub?.markdown.getCall(0).args[0]).to.include( 'Unable to generate a schema from the collection, no documents found.' ); + + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 0, + expectedInternalPurpose: 'namespace', + }); }); }); }); @@ -1423,7 +1514,8 @@ Schema: json: () => Promise.resolve({ _id: '650b4b260f975ef031016c8a', - messages: [], + content: + 'To connect to MongoDB using mongosh, you can follow these steps', }), }); global.fetch = fetchStub; @@ -1435,6 +1527,10 @@ Schema: await invokeChatHandler(chatRequestMock); expect(fetchStub).to.have.been.called; expect(sendRequestStub).to.have.not.been.called; + + assertResponseTelemetry('docs/chatbot', { + callIndex: 0, + }); }); test('falls back to the copilot model when docs chatbot result is not available', async function () { @@ -1454,7 +1550,9 @@ Schema: expect(sendRequestStub).to.have.been.called; // Expect the error to be reported through the telemetry service - expect(telemetryTrackStub).to.have.been.calledTwice; + expect( + telemetryTrackStub.getCalls() + ).to.have.length.greaterThanOrEqual(2); expect(telemetryTrackStub.firstCall.args[0]).to.equal( TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED ); @@ -1462,6 +1560,11 @@ Schema: const properties = telemetryTrackStub.firstCall.args[1]; expect(properties.command).to.equal('docs'); expect(properties.error_name).to.equal('Docs Chatbot API Issue'); + + assertResponseTelemetry('docs/copilot', { + callIndex: 2, + hasCTA: true, + }); }); }); }); From beb8d727dbea3387bb4d21f3a857141c54a51327 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Sat, 28 Sep 2024 00:14:46 +0200 Subject: [PATCH 43/45] docs: update README with information about the participant VSCODE-587 (#841) * docs: update README with information about the participant VSCODE-587 * refactor: wrap participant id * refactor: remove empty line * Update README.md Co-authored-by: Rhys * Update README.md Co-authored-by: Rhys * Update README.md Co-authored-by: Rhys * Update README.md Co-authored-by: Rhys * docs: update screenshot --------- Co-authored-by: Rhys --- README.md | 21 ++++++++++++++++++ .../mongodb-participant-commands.png | Bin 0 -> 58604 bytes resources/screenshots/mongodb-participant.png | Bin 0 -> 57145 bytes 3 files changed, 21 insertions(+) create mode 100644 resources/screenshots/mongodb-participant-commands.png create mode 100644 resources/screenshots/mongodb-participant.png diff --git a/README.md b/README.md index d3233d25f..ecad90e60 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,25 @@ Connect to Atlas Stream Processing instances and develop stream processors using ![Atlas Stream Processing Playground](resources/screenshots/atlas-stream-processing.png) +### MongoDB Copilot Participant + +Use natural language to interact with your clusters and generate MongoDB-related code with GitHub Copilot Chat in VS Code. + +_Note: To use the MongoDB Participant, you must have the [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) extension. By using Copilot Chat you agree to [GitHub Copilot chat preview terms](https://docs.github.com/en/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-your-ide). Find more details about the MongoDB GenAI Features in the [FAQ](https://www.mongodb.com/docs/generative-ai-faq/)._ + +#### How to use the MongoDB Participant + +1. Enter `@MongoDB` in the chat input field to start a conversation with the MongoDB Participant. + +![MongoDB Participant](resources/screenshots/mongodb-participant.png) + +2. Start typing `/` in the chat window to get the list of available chat participant commands. + +![MongoDB Participant Commands](resources/screenshots/mongodb-participant-commands.png) + +- `/docs` is a participant command that finds answers to coding-related questions in the [MongoDB documentation](https://www.mongodb.com/docs/). +- `/query` is a participant command that generates MongoDB queries from natural language to be used with a connected MongoDB cluster. It generates both queries and aggregations depending on the complexity of the request. It utilizes schema to reduce model hallucinations. It provides a code action to open generated code in a playground and an action to directly run the code from the Copilot chat interface. +- `/schema` is a participant command that analyzes and returns information about a collection's schema. ## Extension Settings @@ -99,6 +118,8 @@ Connect to Atlas Stream Processing instances and develop stream processors using | `mdb.useDefaultTemplateForPlayground` | Choose whether to use the default template for playground files or to start with an empty playground editor. | `true` | | `mdb.uniqueObjectIdPerCursor` | The default behavior is to generate a single ObjectId and insert it on all cursors. Set to true to generate a unique ObjectId per cursor instead. | `false` | | `mdb.sendTelemetry` | Opt-in and opt-out for diagnostic and telemetry collection. | `true` | +| `mdb.confirmRunCopilotCode` | Show a confirmation message before running code generated by the MongoDB participant. | `true` | +| `mdb.useSampleDocsInCopilot` | Enable sending sample field values with the VSCode copilot chat @MongoDB participant /query command. | `false` | ## Additional Settings diff --git a/resources/screenshots/mongodb-participant-commands.png b/resources/screenshots/mongodb-participant-commands.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5e9319a6cb8fc1986d9dbeacd3ceaa8cfbdb41 GIT binary patch literal 58604 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≶$+E5C@ssNR*bB~bip_?o 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 literal 0 HcmV?d00001 diff --git a/resources/screenshots/mongodb-participant.png b/resources/screenshots/mongodb-participant.png new file mode 100644 index 0000000000000000000000000000000000000000..69f3d6e2f7fdbe64771068e9189c58c4daae7b1c GIT binary patch literal 57145 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 z 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 literal 0 HcmV?d00001 From 0a5a76fcb9f6e05c6d2adf4c2af1ccfc6f754cfd Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 30 Sep 2024 10:50:04 -0400 Subject: [PATCH 44/45] fix(chat): add message content to blank message request VSCODE-626 (#843) --- src/participant/participant.ts | 85 ++++++- src/participant/prompts/index.ts | 20 +- .../suite/participant/participant.test.ts | 221 +++++++++++------- 3 files changed, 228 insertions(+), 98 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index cbc574b91..519464fab 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { getSimplifiedSchema, parseSchema } from 'mongodb-schema'; import type { Document } from 'bson'; import type { Reference } from 'mongodb-rag-core'; +import util from 'util'; import { createLogger } from '../logging'; import type ConnectionController from '../connectionController'; @@ -138,6 +139,12 @@ export default class ParticipantController { errorName = ParticipantErrorTypes.OTHER; } + log.error('Participant encountered an error', { + command, + error_code: errorCode, + error_name: errorName, + }); + this._telemetryService.track( TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED, { @@ -177,9 +184,26 @@ export default class ParticipantController { throw new Error('Copilot model not found'); } + log.info('Sending request to model', { + messages: modelInput.messages.map( + (message: vscode.LanguageModelChatMessage) => + util.inspect({ + role: message.role, + contentLength: message.content.length, + }) + ), + }); this._telemetryService.trackCopilotParticipantPrompt(modelInput.stats); - return await model.sendRequest(modelInput.messages, {}, token); + const modelResponse = await model.sendRequest( + modelInput.messages, + {}, + token + ); + + log.info('Model response received'); + + return modelResponse; } async streamChatResponse({ @@ -267,6 +291,11 @@ export default class ParticipantController { identifier: codeBlockIdentifier, }); + log.info('Streamed response to chat', { + outputLength, + hasCodeBlock, + }); + return { outputLength, hasCodeBlock, @@ -376,6 +405,10 @@ export default class ParticipantController { token, }); + log.info('Received intent response from model', { + responseContentLength: responseContent.length, + }); + return Prompts.intent.getIntentFromModelResponse(responseContent); } @@ -738,14 +771,41 @@ export default class ParticipantController { request, connectionNames: this._getConnectionNames(), }); - const responseContentWithNamespace = await this.getChatResponseContent({ - modelInput: messagesWithNamespace, - token, - }); - const { databaseName, collectionName } = - Prompts.namespace.extractDatabaseAndCollectionNameFromResponse( - responseContentWithNamespace - ); + + let { + databaseName, + collectionName, + }: { + databaseName: string | undefined; + collectionName: string | undefined; + } = { + databaseName: undefined, + collectionName: undefined, + }; + + // When there's no user message content we can + // skip the request to the model. This would happen with /schema. + if (Prompts.doMessagesContainUserInput(messagesWithNamespace.messages)) { + // VSCODE-626: When there's an empty message sent to the ai model, + // it currently errors (not on insiders, only main VSCode). + // Here we're defaulting to have some content as a workaround. + // TODO: Remove this when the issue is fixed. + messagesWithNamespace.messages[ + messagesWithNamespace.messages.length - 1 + ].content = + messagesWithNamespace.messages[ + messagesWithNamespace.messages.length - 1 + ].content.trim() || 'see previous messages'; + + const responseContentWithNamespace = await this.getChatResponseContent({ + modelInput: messagesWithNamespace, + token, + }); + ({ databaseName, collectionName } = + Prompts.namespace.extractDatabaseAndCollectionNameFromResponse( + responseContentWithNamespace + )); + } // See if there's a namespace set in the // chat metadata we can fallback to if the model didn't find it. @@ -757,6 +817,11 @@ export default class ParticipantController { collectionName: collectionNameFromMetadata, } = this._chatMetadataStore.getChatMetadata(chatId) ?? {}; + log.info('Namespaces found in chat', { + databaseName: databaseName || databaseNameFromMetadata, + collectionName: collectionName || collectionNameFromMetadata, + }); + return { databaseName: databaseName || databaseNameFromMetadata, collectionName: collectionName || collectionNameFromMetadata, @@ -831,6 +896,8 @@ export default class ParticipantController { context: vscode.ChatContext; stream: vscode.ChatResponseStream; }): ChatResult { + log.info('Participant asked user to connect'); + stream.markdown( "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against.\n\n" ); diff --git a/src/participant/prompts/index.ts b/src/participant/prompts/index.ts index 867b057dd..18e4150af 100644 --- a/src/participant/prompts/index.ts +++ b/src/participant/prompts/index.ts @@ -1,4 +1,4 @@ -import type * as vscode from 'vscode'; +import * as vscode from 'vscode'; import { GenericPrompt } from './generic'; import { IntentPrompt } from './intent'; @@ -16,4 +16,22 @@ export class Prompts { public static isPromptEmpty(request: vscode.ChatRequest): boolean { return !request.prompt || request.prompt.trim().length === 0; } + + // Check if any of the messages contain user input. + // This is useful since when there's no user input in any + // messages, we can skip some additional processing. + public static doMessagesContainUserInput( + messages: vscode.LanguageModelChatMessage[] + ): boolean { + for (const message of messages) { + if ( + message.role === vscode.LanguageModelChatMessageRole.User && + message.content.trim().length > 0 + ) { + return true; + } + } + + return false; + } } diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index b46f19294..500f2b356 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1343,26 +1343,70 @@ suite('Participant Controller Test Suite', function () { }); suite('schema command', function () { - suite('known namespace from running namespace LLM', function () { + suite('no namespace provided', function () { beforeEach(function () { sendRequestStub.onCall(0).resolves({ - text: ['DATABASE_NAME: dbOne\n', 'COLLECTION_NAME: collOne\n`'], + text: ['none'], }); }); - test('shows a button to view the json output', async function () { + test('without a prompt it asks for the database name without pinging ai', async function () { const chatRequestMock = { prompt: '', command: 'schema', references: [], }; - sampleStub.resolves([ - { - _id: new ObjectId('63ed1d522d8573fa5c203660'), - }, - ]); await invokeChatHandler(chatRequestMock); - const expectedSchema = `{ + + expect(sendRequestStub.called).to.be.false; + const askForDBMessage = chatStreamStub.markdown.getCall(0).args[0]; + expect(askForDBMessage).to.include( + 'What is the name of the database you would like to run against?' + ); + }); + + test('with a prompt it asks the ai for the namespace', async function () { + const chatRequestMock = { + prompt: 'pineapple', + command: 'schema', + references: [], + }; + await invokeChatHandler(chatRequestMock); + + expect(sendRequestStub.calledOnce).to.be.true; + expect(sendRequestStub.firstCall.args[0][0].content).to.include( + 'Parse all user messages to find a database name and a collection name.' + ); + + const askForDBMessage = chatStreamStub.markdown.getCall(0).args[0]; + expect(askForDBMessage).to.include( + 'What is the name of the database you would like to run against?' + ); + }); + }); + + suite( + 'with a prompt and a known namespace from running namespace LLM', + function () { + beforeEach(function () { + sendRequestStub.onCall(0).resolves({ + text: ['DATABASE_NAME: dbOne\n', 'COLLECTION_NAME: collOne\n`'], + }); + }); + + test('shows a button to view the json output', async function () { + const chatRequestMock = { + prompt: 'what is my schema', + command: 'schema', + references: [], + }; + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203660'), + }, + ]); + await invokeChatHandler(chatRequestMock); + const expectedSchema = `{ "count": 1, "fields": [ { @@ -1388,108 +1432,109 @@ suite('Participant Controller Test Suite', function () { } ] }`; - expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ - command: 'mdb.participantViewRawSchemaOutput', - title: 'Open JSON Output', - arguments: [ - { - schema: expectedSchema, - }, - ], - }); + expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ + command: 'mdb.participantViewRawSchemaOutput', + title: 'Open JSON Output', + arguments: [ + { + schema: expectedSchema, + }, + ], + }); - assertCommandTelemetry('schema', chatRequestMock, { - callIndex: 0, - expectedInternalPurpose: 'namespace', - }); + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 0, + expectedInternalPurpose: 'namespace', + }); - assertCommandTelemetry('schema', chatRequestMock, { - callIndex: 1, - }); + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 1, + }); - assertResponseTelemetry('schema', { - callIndex: 2, - hasCTA: true, - foundNamespace: true, + assertResponseTelemetry('schema', { + callIndex: 2, + hasCTA: true, + foundNamespace: true, + }); }); - }); - test("includes the collection's schema in the request", async function () { - sampleStub.resolves([ - { - _id: new ObjectId('63ed1d522d8573fa5c203660'), - field: { - stringField: - 'There was a house cat who finally got the chance to do what it had always wanted to do.', - arrayField: [new Int32('1')], + test("includes the collection's schema in the request", async function () { + sampleStub.resolves([ + { + _id: new ObjectId('63ed1d522d8573fa5c203660'), + field: { + stringField: + 'There was a house cat who finally got the chance to do what it had always wanted to do.', + arrayField: [new Int32('1')], + }, }, - }, - { - _id: new ObjectId('63ed1d522d8573fa5c203660'), - field: { - stringField: 'Pineapple.', - arrayField: [new Int32('166')], + { + _id: new ObjectId('63ed1d522d8573fa5c203660'), + field: { + stringField: 'Pineapple.', + arrayField: [new Int32('166')], + }, }, - }, - ]); - const chatRequestMock = { - prompt: '', - command: 'schema', - references: [], - }; - await invokeChatHandler(chatRequestMock); - const messages = sendRequestStub.secondCall.args[0]; - expect(messages[0].content).to.include( - 'Amount of documents sampled: 2' - ); - expect(messages[1].content).to.include( - `Database name: dbOne + ]); + const chatRequestMock = { + prompt: 'what is my schema', + command: 'schema', + references: [], + }; + await invokeChatHandler(chatRequestMock); + const messages = sendRequestStub.secondCall.args[0]; + expect(messages[0].content).to.include( + 'Amount of documents sampled: 2' + ); + expect(messages[1].content).to.include( + `Database name: dbOne Collection name: collOne Schema: { "count": 2, "fields": [` - ); - expect(messages[1].content).to.include(`"name": "arrayField", + ); + expect(messages[1].content).to.include(`"name": "arrayField", "path": [ "field", "arrayField" ],`); - assertCommandTelemetry('schema', chatRequestMock, { - callIndex: 0, - expectedInternalPurpose: 'namespace', - }); + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 0, + expectedInternalPurpose: 'namespace', + }); - assertCommandTelemetry('schema', chatRequestMock, { - callIndex: 1, - }); + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 1, + }); - assertResponseTelemetry('schema', { - callIndex: 2, - hasCTA: true, - foundNamespace: true, + assertResponseTelemetry('schema', { + callIndex: 2, + hasCTA: true, + foundNamespace: true, + }); }); - }); - test('prints a message when no documents are found', async function () { - sampleStub.resolves([]); - const chatRequestMock = { - prompt: '', - command: 'schema', - references: [], - }; - await invokeChatHandler(chatRequestMock); - expect(chatStreamStub?.markdown.getCall(0).args[0]).to.include( - 'Unable to generate a schema from the collection, no documents found.' - ); + test('prints a message when no documents are found', async function () { + sampleStub.resolves([]); + const chatRequestMock = { + prompt: 'what is my schema', + command: 'schema', + references: [], + }; + await invokeChatHandler(chatRequestMock); + expect(chatStreamStub?.markdown.getCall(0).args[0]).to.include( + 'Unable to generate a schema from the collection, no documents found.' + ); - assertCommandTelemetry('schema', chatRequestMock, { - callIndex: 0, - expectedInternalPurpose: 'namespace', + assertCommandTelemetry('schema', chatRequestMock, { + callIndex: 0, + expectedInternalPurpose: 'namespace', + }); }); - }); - }); + } + ); }); suite('docs command', function () { From 547e4552924b0ed9707ee6af24f412a3efa50b8b Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 30 Sep 2024 11:02:08 -0400 Subject: [PATCH 45/45] fix(chat): avoid passing file path when its not used in playground run (#844) --- package.json | 10 +- src/commands/index.ts | 4 +- src/editors/playgroundController.ts | 101 ++++++++++++------ src/mdbExtensionController.ts | 14 +-- src/participant/participant.ts | 8 +- .../editors/playgroundController.test.ts | 6 +- .../suite/participant/participant.test.ts | 8 +- .../suite/telemetry/telemetryService.test.ts | 4 +- 8 files changed, 98 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index f4f9bfd43..3e5970436 100644 --- a/package.json +++ b/package.json @@ -192,11 +192,11 @@ "title": "MongoDB: Change Active Connection with Participant" }, { - "command": "mdb.runParticipantQuery", + "command": "mdb.runParticipantCode", "title": "Run Content Generated by Participant" }, { - "command": "mdb.openParticipantQueryInPlayground", + "command": "mdb.openParticipantCodeInPlayground", "title": "Open Generated by Participant Content In Playground" }, { @@ -766,15 +766,15 @@ "when": "false" }, { - "command": "mdb.runParticipantQuery", + "command": "mdb.runParticipantCode", "when": "false" }, { - "command": "mdb.openParticipantQueryInPlayground", + "command": "mdb.openParticipantCodeInPlayground", "when": "false" }, { - "command": "mdb.runParticipantQuery", + "command": "mdb.runParticipantCode", "when": "false" }, { diff --git a/src/commands/index.ts b/src/commands/index.ts index 767b99ffb..c36c477d2 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -77,8 +77,8 @@ enum EXTENSION_COMMANDS { MDB_DROP_STREAM_PROCESSOR = 'mdb.dropStreamProcessor', // Chat participant. - OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND = 'mdb.openParticipantQueryInPlayground', - RUN_PARTICIPANT_QUERY = 'mdb.runParticipantQuery', + OPEN_PARTICIPANT_CODE_IN_PLAYGROUND = 'mdb.openParticipantCodeInPlayground', + RUN_PARTICIPANT_CODE = 'mdb.runParticipantCode', CONNECT_WITH_PARTICIPANT = 'mdb.connectWithParticipant', SELECT_DATABASE_WITH_PARTICIPANT = 'mdb.selectDatabaseWithParticipant', SELECT_COLLECTION_WITH_PARTICIPANT = 'mdb.selectCollectionWithParticipant', diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 505459191..118a7a376 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -59,10 +59,14 @@ interface ToCompile { let dummySandbox; +function getActiveEditorFilePath(): string | undefined { + return vscode.window.activeTextEditor?.document.uri.fsPath; +} + // TODO: this function was copied from the compass-export-to-language module // https://github.com/mongodb-js/compass/blob/7c4bc0789a7b66c01bb7ba63955b3b11ed40c094/packages/compass-export-to-language/src/modules/count-aggregation-stages-in-string.js // and should be updated as well when the better solution for the problem will be found. -const countAggregationStagesInString = (str: string) => { +const countAggregationStagesInString = (str: string): number => { if (!dummySandbox) { dummySandbox = vm.createContext(Object.create(null), { codeGeneration: { strings: false, wasm: false }, @@ -112,6 +116,9 @@ const exportModeMapping: Record< [ExportToLanguageMode.OTHER]: undefined, }; +const connectBeforeRunningMessage = + 'Please connect to a database before running a playground.'; + /** * This controller manages playground. */ @@ -160,7 +167,7 @@ export default class PlaygroundController { this._playgroundSelectedCodeActionProvider = playgroundSelectedCodeActionProvider; - this._activeConnectionChangedHandler = () => { + this._activeConnectionChangedHandler = (): void => { void this._activeConnectionChanged(); }; this._connectionController.addEventListener( @@ -170,7 +177,7 @@ export default class PlaygroundController { const onDidChangeActiveTextEditor = ( editor: vscode.TextEditor | undefined - ) => { + ): void => { if (editor?.document.uri.scheme === PLAYGROUND_RESULT_SCHEME) { this._playgroundResultViewColumn = editor.viewColumn; this._playgroundResultTextDocument = editor?.document; @@ -373,7 +380,7 @@ export default class PlaygroundController { return this._createPlaygroundFileWithContent(content); } - createPlaygroundFromParticipantQuery({ + createPlaygroundFromParticipantCode({ text, }: { text: string; @@ -438,13 +445,17 @@ export default class PlaygroundController { return this._createPlaygroundFileWithContent(content); } - async _evaluate(codeToEvaluate: string): Promise { + async _evaluate({ + codeToEvaluate, + filePath, + }: { + codeToEvaluate: string; + filePath?: string; + }): Promise { const connectionId = this._connectionController.getActiveConnectionId(); if (!connectionId) { - throw new Error( - 'Please connect to a database before running a playground.' - ); + throw new Error(connectBeforeRunningMessage); } this._statusView.showMessage('Getting results...'); @@ -455,7 +466,7 @@ export default class PlaygroundController { result = await this._languageServerController.evaluate({ codeToEvaluate, connectionId, - filePath: vscode.window.activeTextEditor?.document.uri.fsPath, + filePath, }); } catch (error) { const msg = @@ -482,13 +493,15 @@ export default class PlaygroundController { return this._activeTextEditor?.document.getText(selection) || ''; } - async _evaluateWithCancelModal( - codeToEvaluate: string - ): Promise { + async _evaluateWithCancelModal({ + codeToEvaluate, + filePath, + }: { + codeToEvaluate: string; + filePath?: string; + }): Promise { if (!this._connectionController.isCurrentlyConnected()) { - throw new Error( - 'Please connect to a database before running a playground.' - ); + throw new Error(connectBeforeRunningMessage); } try { @@ -507,9 +520,10 @@ export default class PlaygroundController { }); // Run all playground scripts. - const result: ShellEvaluateResult = await this._evaluate( - codeToEvaluate - ); + const result: ShellEvaluateResult = await this._evaluate({ + codeToEvaluate, + filePath, + }); return result; } @@ -572,11 +586,18 @@ export default class PlaygroundController { } } - async evaluateParticipantQuery(text: string): Promise { + async evaluateParticipantCode(codeToEvaluate: string): Promise { const shouldConfirmRunCopilotCode = vscode.workspace .getConfiguration('mdb') .get('confirmRunCopilotCode'); + if (!this._connectionController.isCurrentlyConnected()) { + // TODO(VSCODE-618): Prompt user to connect when clicked. + void vscode.window.showErrorMessage(connectBeforeRunningMessage); + + return false; + } + if (shouldConfirmRunCopilotCode === true) { const name = this._connectionController.getActiveConnectionName(); const confirmRunCopilotCode = await vscode.window.showInformationMessage( @@ -591,7 +612,9 @@ export default class PlaygroundController { } const evaluateResponse: ShellEvaluateResult = - await this._evaluateWithCancelModal(text); + await this._evaluateWithCancelModal({ + codeToEvaluate, + }); if (!evaluateResponse || !evaluateResponse.result) { return false; @@ -602,15 +625,19 @@ export default class PlaygroundController { return true; } - async _evaluatePlayground(text: string): Promise { + async _evaluatePlayground({ + codeToEvaluate, + filePath, + }: { + codeToEvaluate: string; + filePath?: string; + }): Promise { const shouldConfirmRunAll = vscode.workspace .getConfiguration('mdb') .get('confirmRunAll'); if (!this._connectionController.isCurrentlyConnected()) { - void vscode.window.showErrorMessage( - 'Please connect to a database before running a playground.' - ); + void vscode.window.showErrorMessage(connectBeforeRunningMessage); return false; } @@ -629,7 +656,10 @@ export default class PlaygroundController { } const evaluateResponse: ShellEvaluateResult = - await this._evaluateWithCancelModal(text); + await this._evaluateWithCancelModal({ + codeToEvaluate, + filePath, + }); if (!evaluateResponse || !evaluateResponse.result) { return false; @@ -652,7 +682,10 @@ export default class PlaygroundController { this._isPartialRun = true; - return this._evaluatePlayground(this._selectedText || ''); + return this._evaluatePlayground({ + codeToEvaluate: this._selectedText || '', + filePath: getActiveEditorFilePath(), + }); } runAllPlaygroundBlocks(): Promise { @@ -669,7 +702,10 @@ export default class PlaygroundController { this._isPartialRun = false; - return this._evaluatePlayground(this._getAllText()); + return this._evaluatePlayground({ + codeToEvaluate: this._getAllText(), + filePath: getActiveEditorFilePath(), + }); } runAllOrSelectedPlaygroundBlocks(): Promise { @@ -693,14 +729,17 @@ export default class PlaygroundController { codeToEvaluate = this._selectedText; } - return this._evaluatePlayground(codeToEvaluate); + return this._evaluatePlayground({ + codeToEvaluate, + filePath: getActiveEditorFilePath(), + }); } async fixThisInvalidInteractiveSyntax({ documentUri, range, fix, - }: ThisDiagnosticFix) { + }: ThisDiagnosticFix): Promise { const edit = new vscode.WorkspaceEdit(); edit.replace(documentUri, range, fix); await vscode.workspace.applyEdit(edit); @@ -710,7 +749,7 @@ export default class PlaygroundController { async fixAllInvalidInteractiveSyntax({ documentUri, diagnostics, - }: AllDiagnosticFixes) { + }: AllDiagnosticFixes): Promise { const edit = new vscode.WorkspaceEdit(); for (const { range, fix } of diagnostics) { @@ -884,7 +923,7 @@ export default class PlaygroundController { language, num_stages: selectedText ? countAggregationStagesInString(selectedText) - : null, + : undefined, with_import_statements: importStatements, with_builders: builders, with_driver_syntax: driverSyntax, diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 7103183ee..e481a57ea 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -44,7 +44,7 @@ import { ConnectionStorage } from './storage/connectionStorage'; import type StreamProcessorTreeItem from './explorer/streamProcessorTreeItem'; import type { ParticipantCommand, - RunParticipantQueryCommandArgs, + RunParticipantCodeCommandArgs, } from './participant/participant'; import ParticipantController from './participant/participant'; import type { OpenSchemaCommandArgs } from './participant/prompts/schema'; @@ -296,17 +296,17 @@ export default class MDBExtensionController implements vscode.Disposable { // ------ CHAT PARTICIPANT ------ // this.registerParticipantCommand( - EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, - ({ runnableContent }: RunParticipantQueryCommandArgs) => { - return this._playgroundController.createPlaygroundFromParticipantQuery({ + EXTENSION_COMMANDS.OPEN_PARTICIPANT_CODE_IN_PLAYGROUND, + ({ runnableContent }: RunParticipantCodeCommandArgs) => { + return this._playgroundController.createPlaygroundFromParticipantCode({ text: runnableContent, }); } ); this.registerParticipantCommand( - EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, - ({ runnableContent }: RunParticipantQueryCommandArgs) => { - return this._playgroundController.evaluateParticipantQuery( + EXTENSION_COMMANDS.RUN_PARTICIPANT_CODE, + ({ runnableContent }: RunParticipantCodeCommandArgs) => { + return this._playgroundController.evaluateParticipantCode( runnableContent ); } diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 519464fab..79c3f16bd 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -55,7 +55,7 @@ interface NamespaceQuickPicks { data: string; } -export type RunParticipantQueryCommandArgs = { +export type RunParticipantCodeCommandArgs = { runnableContent: string; }; @@ -244,16 +244,16 @@ export default class ParticipantController { return; } - const commandArgs: RunParticipantQueryCommandArgs = { + const commandArgs: RunParticipantCodeCommandArgs = { runnableContent, }; stream.button({ - command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY, + command: EXTENSION_COMMANDS.RUN_PARTICIPANT_CODE, title: vscode.l10n.t('▶️ Run'), arguments: [commandArgs], }); stream.button({ - command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND, + command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_CODE_IN_PLAYGROUND, title: vscode.l10n.t('Open in playground'), arguments: [commandArgs], }); diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index 7d5458161..125ba4a7e 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -324,9 +324,9 @@ suite('Playground Controller Test Suite', function () { sandbox.fake.rejects(false) ); - const result = await testPlaygroundController._evaluateWithCancelModal( - '' - ); + const result = await testPlaygroundController._evaluateWithCancelModal({ + codeToEvaluate: '', + }); expect(result).to.deep.equal({ result: undefined }); }); diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 500f2b356..12ba8ae46 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -563,7 +563,7 @@ suite('Participant Controller Test Suite', function () { expect(res?.metadata.intent).to.equal('generic'); expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ - command: 'mdb.runParticipantQuery', + command: 'mdb.runParticipantCode', title: '▶️ Run', arguments: [ { @@ -605,7 +605,7 @@ suite('Participant Controller Test Suite', function () { }; await invokeChatHandler(chatRequestMock); expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ - command: 'mdb.runParticipantQuery', + command: 'mdb.runParticipantCode', title: '▶️ Run', arguments: [ { @@ -1141,7 +1141,7 @@ suite('Participant Controller Test Suite', function () { expect(chatStreamStub?.button.callCount).to.equal(2); expect(chatStreamStub?.button.getCall(0).args[0]).to.deep.equal({ - command: 'mdb.runParticipantQuery', + command: 'mdb.runParticipantCode', title: '▶️ Run', arguments: [ { @@ -1151,7 +1151,7 @@ suite('Participant Controller Test Suite', function () { ], }); expect(chatStreamStub?.button.getCall(1).args[0]).to.deep.equal({ - command: 'mdb.openParticipantQueryInPlayground', + command: 'mdb.openParticipantCodeInPlayground', title: 'Open in playground', arguments: [ { diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index 0055f0e50..f6e78e374 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -235,7 +235,9 @@ suite('Telemetry Controller Test Suite', () => { test('track playground code executed event', async () => { const testPlaygroundController = mdbTestExtension.testExtensionController._playgroundController; - await testPlaygroundController._evaluate('show dbs'); + await testPlaygroundController._evaluate({ + codeToEvaluate: 'show dbs', + }); sandbox.assert.calledWith( fakeSegmentAnalyticsTrack, sinon.match({