Path: blob/main/extensions/copilot/src/extension/prompt/node/devContainerConfigGenerator.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type { CancellationToken } from 'vscode';6import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';7import { DevContainerConfigFeature, DevContainerConfigGeneratorResult, DevContainerConfigIndex, DevContainerConfigTemplate } from '../../../platform/devcontainer/common/devContainerConfigurationService';8import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';9import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';10import { escapeRegExpCharacters } from '../../../util/vs/base/common/strings';11import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';12import { PromptRenderer } from '../../prompts/node/base/promptRenderer';13import { DevContainerConfigPrompt } from '../../prompts/node/devcontainer/devContainerConfigPrompt';1415const excludedTemplates = [16'alpine',17'debian',18'docker-existing-docker-compose',19'docker-existing-dockerfile',20'docker-in-docker',21'docker-outside-of-docker',22'docker-outside-of-docker-compose',23'ubuntu',24'universal',25].map(shortId => `ghcr.io/devcontainers/templates/${shortId}`);2627const excludedFeatures = [28'common-utils',29'git',30].map(shortId => `ghcr.io/devcontainers/features/${shortId}`);3132export class DevContainerConfigGenerator {3334constructor(35@ITelemetryService private readonly telemetryService: ITelemetryService,36@IEndpointProvider private readonly endpointProvider: IEndpointProvider,37@IInstantiationService private readonly instantiationService: IInstantiationService,38) { }3940async generate(index: DevContainerConfigIndex, filenames: string[], token: CancellationToken): Promise<DevContainerConfigGeneratorResult> {41if (!filenames.length) {42return {43type: 'success',44template: undefined,45features: [],46};47}4849const startTime = Date.now();5051const endpoint = await this.endpointProvider.getChatEndpoint('copilot-base');52const charLimit = Math.floor((endpoint.modelMaxPromptTokens * 4) / 3);5354const processedFilenames = this.processFilenames(filenames, charLimit);5556const templates = index.templates.filter(template => !excludedTemplates.includes(template.id));57const features = (index.features || []).filter(feature => !excludedFeatures.includes(feature.id));5859const promptRenderer = PromptRenderer.create(this.instantiationService, endpoint, DevContainerConfigPrompt, {60filenames: processedFilenames,61templates,62features,63});64const prompt = await promptRenderer.render();6566const requestStartTime = Date.now();67const fetchResult = await endpoint68.makeChatRequest(69'devContainerConfigGenerator',70prompt.messages,71undefined,72token,73ChatLocation.Other,74);7576const suggestions = fetchResult.type === ChatFetchResponseType.Success ? this.processGeneratedConfig(fetchResult.value, templates, features) : undefined;7778/* __GDPR__79"devcontainer.generateConfig" : {80"owner": "chrmarti",81"comment": "Metadata about the Dev Container Config generation",82"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that is used in the endpoint." },83"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },84"responseType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The result type of the response." },85"templateId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The chosen template id." },86"featureIds": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The chosen feature ids." },87"originalFilenameCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of filenames." },88"originalFilenameLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The length of the filenames." },89"processedFilenameCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of filenames after processing." },90"processedFilenameLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The length of the filenames after processing." },91"timeToRequest": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "How long it took to start the request." },92"timeToComplete": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "How long it took to complete the request." }93}94*/95this.telemetryService.sendMSFTTelemetryEvent('devcontainer.generateConfig', {96model: endpoint.model,97requestId: fetchResult.requestId,98responseType: fetchResult.type,99templateId: suggestions?.template,100featureIds: suggestions?.features.join(','),101}, {102originalFilenameCount: filenames.length,103originalFilenameLength: filenames.join('').length,104processedFilenameCount: processedFilenames.length,105processedFilenameLength: processedFilenames.join('').length,106timeToRequest: requestStartTime - startTime,107timeToComplete: Date.now() - startTime108});109110return {111type: 'success',112template: suggestions?.template,113features: suggestions?.features || [],114};115}116117private processFilenames(filenames: string[], charLimit: number): string[] {118const result: string[] = [...filenames];119120// Reserve 10% of the character limit for the safety rules and instructions121const availableChars = Math.floor(charLimit * 0.9);122123// Remove filenames if needed124let totalChars = result.join('\n').length;125if (totalChars > availableChars) {126// Remove filenames until we are under the character limit127while (totalChars > availableChars && result.length > 0) {128const lastDiff = result.pop()!;129totalChars -= lastDiff.length;130}131}132133return result;134}135136private processGeneratedConfig(message: string, availableTemplates: DevContainerConfigTemplate[], availableFeatures: DevContainerConfigFeature[]) {137let template = availableTemplates.find(t => new RegExp(`\\b${escapeRegExpCharacters(t.id)}\\b`).test(message))?.id;138if (template === 'ghcr.io/devcontainers/templates/javascript-node') {139template = 'ghcr.io/devcontainers/templates/typescript-node'; // Rarely suggested otherwise.140}141return {142template: template,143features: availableFeatures.filter(f => new RegExp(`\\b${escapeRegExpCharacters(f.id)}\\b`).test(message)).map(f => f.id),144};145}146}147148149