Path: blob/main/extensions/copilot/src/extension/context/node/resolvers/promptWorkspaceLabels.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';7import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';8import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';9import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';10import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';11import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';12import { createServiceIdentifier } from '../../../../util/common/services';13import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';14import { Uri } from '../../../../vscodeTypes';1516export const IPromptWorkspaceLabels = createServiceIdentifier<IPromptWorkspaceLabels>('IPromptWorkspaceLabels');17export interface IPromptWorkspaceLabels {18readonly _serviceBrand: undefined;19/**20* Will be unique and sorted.21*/22readonly labels: string[];23collectContext(): Promise<void>;24}2526const enum PromptWorkspaceLabelsStrategy {27Basic,28Expanded29}3031export class PromptWorkspaceLabels implements IPromptWorkspaceLabels {32declare _serviceBrand: undefined;3334private readonly basicWorkspaceLabels: IPromptWorkspaceLabelsStrategy;35private readonly expandedWorkspaceLabels: IPromptWorkspaceLabelsStrategy;36private strategy = PromptWorkspaceLabelsStrategy.Basic;3738private get workspaceLabels(): IPromptWorkspaceLabelsStrategy {39return this.strategy === PromptWorkspaceLabelsStrategy.Basic ? this.basicWorkspaceLabels : this.expandedWorkspaceLabels;40}4142constructor(43@IExperimentationService private readonly _experimentationService: IExperimentationService,44@IConfigurationService private readonly _configurationService: IConfigurationService,45@ITelemetryService private readonly _telemetryService: ITelemetryService,46@IInstantiationService private readonly _instantiationService: IInstantiationService,47) {48this.basicWorkspaceLabels = this._instantiationService.createInstance(BasicPromptWorkspaceLabels);49this.expandedWorkspaceLabels = this._instantiationService.createInstance(ExpandedPromptWorkspaceLabels);50}5152public get labels(): string[] {53const uniqueLabels = [...new Set(this.workspaceLabels.labels)].sort();54return uniqueLabels;55}5657public async collectContext(): Promise<void> {58const expandedLabels = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.ProjectLabelsExpanded, this._experimentationService);59this.strategy = expandedLabels ? PromptWorkspaceLabelsStrategy.Expanded : PromptWorkspaceLabelsStrategy.Basic;60await this.workspaceLabels.collectContext();6162const uniqueLabels = [...new Set(this.labels)].sort();6364/* __GDPR__65"projectLabels" : {66"owner": "digitarald",67"comment": "Reports quality of labels detected in a workspace",68"labels": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Unique workspace label count." },69"count": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Unique workspace labels in context." }70}71*/72this._telemetryService.sendMSFTTelemetryEvent('projectLabels', {73labels: uniqueLabels.join(',').replaceAll('@', ' ')74}, {75count: uniqueLabels.length,76});77}78}7980interface IPromptWorkspaceLabelsStrategy {81readonly labels: string[];82collectContext(): Promise<void>;83}8485class BasicPromptWorkspaceLabels implements IPromptWorkspaceLabelsStrategy {8687indicators: Map<string, string[]> = new Map<string, string[]>();88contentIndicators: Map<string, (contents: string) => string[]> = new Map<string, (contents: string) => string[]>();89private readonly _labels: string[] = [];9091constructor(92@IWorkspaceService private readonly _workspaceService: IWorkspaceService,93@IFileSystemService private readonly _fileSystemService: IFileSystemService,94@IIgnoreService private readonly _ignoreService: IIgnoreService,95) {96this.initIndicators();97}9899public get labels(): string[] {100// Check if labels have both javascript and typescript and remove javascript101// This can confuse the LLM and typescript should take precedent so types are returned.102if (this._labels.includes('javascript') && this._labels.includes('typescript')) {103const index = this._labels.indexOf('javascript');104this._labels.splice(index, 1);105}106return this._labels;107}108109public async collectContext() {110const folders = this._workspaceService.getWorkspaceFolders();111if (folders) {112for (let i = 0; i < folders.length; i++) {113await this.addContextForFolders(folders[i]);114}115}116}117118private async addContextForFolders(f: Uri) {119for (const [filename, labels] of this.indicators.entries()) {120await this.addLabelIfApplicable(f, filename, labels);121}122}123124private async addLabelIfApplicable(rootFolder: Uri, filename: string, labels: string[]) {125const uri = Uri.joinPath(rootFolder, filename);126127if (await this._ignoreService.isCopilotIgnored(uri)) {128return;129}130131try {132await this._fileSystemService.stat(uri);133labels.forEach(label => this._labels.push(label));134const parseCallback = this.contentIndicators.get(filename);135if (parseCallback) {136const b = await this._fileSystemService.readFile(uri);137try {138const contentLabels = parseCallback(new TextDecoder().decode(b));139contentLabels.forEach(label => this._labels.push(label));140} catch (e) {141// it's ok if we can't parse those files142}143}144} catch (e) {145// ignore non-existing files146}147}148149private initIndicators() {150this.addIndicator('package.json', 'javascript', 'npm');151this.addIndicator('tsconfig.json', 'typescript');152this.addIndicator('pom.xml', 'java', 'maven');153this.addIndicator('build.gradle', 'java', 'gradle');154this.addIndicator('requirements.txt', 'python', 'pip');155this.addIndicator('Pipfile', 'python', 'pip');156this.addIndicator('Cargo.toml', 'rust', 'cargo');157this.addIndicator('go.mod', 'go', 'go.mod');158this.addIndicator('pubspec.yaml', 'dart', 'pub');159this.addIndicator('build.sbt', 'scala', 'sbt');160this.addIndicator('build.boot', 'clojure', 'boot');161this.addIndicator('project.clj', 'clojure', 'lein');162this.addIndicator('mix.exs', 'elixir', 'mix');163this.addIndicator('composer.json', 'php', 'composer');164this.addIndicator('Gemfile', 'ruby', 'bundler');165this.addIndicator('build.xml', 'java', 'ant');166this.addIndicator('build.gradle.kts', 'java', 'gradle');167this.addIndicator('yarn.lock', 'yarn');168this.addIndicator('CMakeLists.txt', 'c++', 'cmake');169this.addIndicator('vcpkg.json', 'c++');170this.addIndicator('Makefile', 'c++', 'makefile');171this.addContentIndicator('CMakeLists.txt', this.collectCMakeListsTxtIndicators);172this.addContentIndicator('package.json', this.collectPackageJsonIndicators);173}174175private addIndicator(filename: string, ...labels: string[]) {176this.indicators.set(filename, labels);177}178179protected addContentIndicator(filename: string, callback: (contents: string) => string[]) {180this.contentIndicators.set(filename, callback);181}182183private collectCMakeListsTxtIndicators(contents: string): string[] {184function parseStandardVersion(contents: string, regex: RegExp, allowedList: number[]): number | undefined {185try {186const matchResult = Array.from(contents.matchAll(regex));187if (matchResult && matchResult[0] && matchResult[0][1]) {188const version = parseInt(matchResult[0][1]);189if (allowedList.includes(version)) {190return version;191}192}193} catch (e) {194// It's ok if the parsing of the standard version fails.195}196return undefined;197}198199const tags: string[] = [];200const cppLangStdVer = parseStandardVersion(contents,201/set\s*\(\s*CMAKE_CXX_STANDARD\s*(\d+)/gmi, [98, 11, 14, 17, 20, 23, 26]);202if (cppLangStdVer) {203tags.push(`C++${cppLangStdVer}`);204}205206const cLangStdVer = parseStandardVersion(contents,207/set\s*\(\s*CMAKE_C_STANDARD\s*(\d+)/gmi, [90, 99, 11, 17, 23]);208if (cLangStdVer) {209tags.push(`C${cLangStdVer}`);210}211return tags;212}213214private collectPackageJsonIndicators(contents: string): string[] {215const tags = [];216const json = JSON.parse(contents);217const dependencies = json.dependencies;218const devDependencies = json.devDependencies;219if (dependencies) {220if (dependencies['@angular/core']) {221tags.push('angular');222}223if (dependencies['react']) {224tags.push('react');225}226if (dependencies['vue']) {227tags.push('vue');228}229}230if (devDependencies) {231if (devDependencies['typescript']) {232tags.push('typescript');233}234}235const engines = json.engines;236if (engines) {237if (engines['node']) {238tags.push('node');239}240if (engines['vscode']) {241tags.push('vscode extension');242}243}244return tags;245}246}247248class ExpandedPromptWorkspaceLabels extends BasicPromptWorkspaceLabels {249250constructor(251@IWorkspaceService workspaceService: IWorkspaceService,252@IFileSystemService fileSystemService: IFileSystemService,253@IIgnoreService ignoreService: IIgnoreService,254) {255super(workspaceService, fileSystemService, ignoreService);256this.addContentIndicator('package.json', this.collectPackageJsonIndicatorsExpanded);257this.addContentIndicator('requirements.txt', this.collectPythonRequirementsIndicators);258this.addContentIndicator('pyproject.toml', this.collectPythonTomlIndicators);259}260261protected collectPackageJsonIndicatorsExpanded(contents: string): string[] {262const tags: string[] = [];263264const extractMajorMinorVersion = (version: string): string => {265const [major, minor] = version.split('.');266return `${major.replace(/[^0-9]/g, '')}.${minor.replace(/[^0-9]/g, '')}`;267};268269const checkDependencies = (dependencies: Record<string, string> | undefined, list: { dependency: string; prefix?: string }[]) => {270if (!dependencies) { return; }271list.forEach(({ dependency, prefix }) => {272if (dependencies[dependency]) {273const version = extractMajorMinorVersion(dependencies[dependency]);274tags.push(`${prefix || dependency}@${version}`);275}276});277};278279let json: any;280try {281json = JSON.parse(contents);282} catch {283return tags;284}285286const allDependenciesFields = [287json.dependencies,288json.devDependencies,289json.peerDependencies,290json.optionalDependencies291];292293const dependenciesList = [294// Frontend Frameworks295{ dependency: 'react' },296{ dependency: 'vue' },297{ dependency: '@angular/core' },298{ dependency: 'svelte' },299{ dependency: 'solid-js' },300{ dependency: 'alpinejs' },301302// State Management Libraries303{ dependency: 'redux' },304{ dependency: 'mobx' },305{ dependency: 'vuex' },306{ dependency: 'ngrx' },307308// UI Libraries309{ dependency: 'antd' },310{ dependency: 'bootstrap' },311{ dependency: 'bulma' },312{ dependency: '@mui/material' },313{ dependency: 'semantic-ui-react' },314315// Rendering Frameworks316{ dependency: 'next' },317{ dependency: 'gatsby' },318{ dependency: 'remix' },319{ dependency: 'astro' },320{ dependency: 'sveltekit' },321{ dependency: 'nuxt' },322323// Testing Tools324{ dependency: 'jest' },325{ dependency: 'mocha' },326{ dependency: 'cypress' },327{ dependency: '@testing-library/react' },328{ dependency: '@playwright/test' },329{ dependency: 'vitest' },330{ dependency: '@storybook/react' },331332// CSS Tools333{ dependency: 'tailwindcss' },334{ dependency: 'sass' },335{ dependency: 'styled-components' },336{ dependency: 'css-modules' },337{ dependency: 'postcss' },338{ dependency: '@emotion/react' },339340// Build Tools341{ dependency: 'vite' },342{ dependency: 'webpack' },343{ dependency: 'parcel' },344{ dependency: 'rollup' },345{ dependency: 'snowpack' },346{ dependency: 'esbuild' },347{ dependency: '@swc/core' },348349// Real-time Communication350{ dependency: 'socket.io' },351352// API and Data Handling353{ dependency: 'd3' },354{ dependency: 'graphql' },355356// Utility Libraries357{ dependency: 'lodash' },358{ dependency: 'moment' },359{ dependency: 'rxjs' },360{ dependency: 'underscore' },361362// Task Runners363{ dependency: 'gulp' },364365// Older Libraries366{ dependency: 'backbone' },367{ dependency: 'ember-source' },368{ dependency: 'handlebars' },369{ dependency: 'jquery' },370{ dependency: 'knockout' },371372// Cloud SDKs373{ dependency: 'aws-sdk' },374{ dependency: 'cloudinary' },375{ dependency: 'firebase' },376{ dependency: '@azure/storage-blob' },377{ dependency: '@google-cloud/storage' },378379// Cloud Functions380{ dependency: '@aws-lambda' },381{ dependency: '@azure/functions' },382{ dependency: '@google-cloud/functions' },383{ dependency: 'firebase-functions' },384385// Cloud Databases386{ dependency: '@azure/cosmos' },387{ dependency: '@google-cloud/firestore' },388{ dependency: 'mongoose' },389390// Containerization and Orchestration391{ dependency: 'dockerode' },392{ dependency: 'kubernetes-client' },393394// Monitoring and Logging395{ dependency: '@elastic/elasticsearch' },396{ dependency: '@sentry/node' },397{ dependency: 'log4js' },398{ dependency: 'winston' },399400// Security401{ dependency: 'bcrypt' },402{ dependency: 'helmet' },403{ dependency: 'jsonwebtoken' },404{ dependency: 'passport' },405406// Azure Libraries407{ dependency: '@azure/identity' },408{ dependency: '@azure/keyvault-certificates' },409{ dependency: '@azure/keyvault-keys' },410{ dependency: '@azure/keyvault-secrets' },411{ dependency: '@azure/service-bus' },412{ dependency: '@azure/event-hubs' },413{ dependency: '@azure/data-tables' },414{ dependency: '@azure/monitor-query' },415{ dependency: '@azure/app-configuration' },416417// Development Tools418{ dependency: 'babel' },419{ dependency: 'eslint' },420{ dependency: 'parcel' },421{ dependency: 'prettier' },422{ dependency: 'rollup' },423{ dependency: 'typescript' },424{ dependency: 'webpack' },425{ dependency: 'vite' },426];427428const enginesList = [429// Engines430{ dependency: 'node' },431{ dependency: 'vscode', prefix: 'vscode extension' }432];433434allDependenciesFields.forEach((deps) => checkDependencies(deps, dependenciesList));435checkDependencies(json.engines, enginesList);436437return tags;438}439440441private popularPackages: string[] = [442// Data Science and Machine Learning443'numpy', 'pandas', 'scipy', 'scikit-learn', 'matplotlib', 'tensorflow', 'keras',444'torch', 'seaborn', 'plotly', 'dash', 'jupyter', 'notebook', 'ipython', 'openai', 'pyspark',445'airflow', 'nltk', 'sympy', 'spacy', 'langchain',446447// Web Development448'Flask', 'Django', 'fastapi', 'pydantic', 'requests', 'beautifulsoup4',449'gunicorn', 'uvicorn', 'httpx', 'Jinja2', 'aiohttp',450451// Testing452'pytest', 'tox', 'nox', 'selenium', 'playwright', 'coverage', 'hypothesis',453454// Documentation455'Sphinx',456457// Task Queue458'celery', 'asyncio',459460// Cloud and DevOps461'boto3', 'google-cloud-storage', 'azure-storage-blob', 'docker', 'kubernetes', 'azure', 'google', 'ansible',462463// Security464'cryptography', 'paramiko', 'PyJWT',465466// Enterprise, Legacy & data storage467'xlrd', 'xlrd-2024', 'openpyxl', 'pywin32', 'pywin', 'psycopg2', 'mysqlclient', 'SQLite4', 'Werkzeug', 'pymongo', 'redis', 'PyMySQL',468469// Utilities470'Pillow', 'SQLAlchemy', 'lxml', 'html5lib', 'Markdown', 'pytz', 'Click',471'attrs', 'PyYAML', 'configparser', 'loguru', 'structlog', 'pygame', 'discord'472];473474475private collectPythonRequirementsIndicators(contents: string): string[] {476const tags: string[] = [];477478const lines = contents.split('\n');479lines.forEach(line => {480const [pkg, version] = line.split('==');481if (this.popularPackages.includes(pkg)) {482tags.push(`${pkg}-${version || 'latest'}`);483}484});485486return tags;487}488489private collectPythonTomlIndicators(contents: string): string[] {490const tags: string[] = [];491492const lines = contents.split('\n');493let inDependenciesSection = false;494495// TODO@digitarald: Should use npm `toml` package, but this is avoiding a dependency for now496lines.forEach(line => {497line = line.trim();498if (line === '[tool.poetry.dependencies]') {499inDependenciesSection = true;500} else if (line.startsWith('[') && line.endsWith(']')) {501inDependenciesSection = false;502} else if (inDependenciesSection && line) {503const [pkg, version] = line.split('=').map(s => s.trim().replace(/"|'/g, ''));504if (this.popularPackages.includes(pkg)) {505tags.push(`${pkg}-${version || 'latest'}`);506}507}508});509510return tags;511}512}513514515