Skip to content

Commit 48e45a3

Browse files
authored
Port Python tools, and create a tool to quickly create a virtual env (#448)
1 parent 78b3142 commit 48e45a3

17 files changed

+652
-741
lines changed

build/.mocha.unittests.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"spec": "./out/test/**/*.unit.test.js",
3-
"require": ["out/test/unittests.js"],
3+
"require": ["source-map-support/register", "out/test/unittests.js"],
44
"ui": "tdd",
55
"recursive": true,
66
"colors": true,

package.json

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
}
2626
},
2727
"activationEvents": [
28-
"onLanguage:python"
28+
"onLanguage:python",
29+
"onLanguageModelTool:get_python_environment_info",
30+
"onLanguageModelTool:get_python_executable_info",
31+
"onLanguageModelTool:install_python_package",
32+
"onLanguageModelTool:create_quick_virtual_environment"
2933
],
3034
"homepage": "https://github.com/microsoft/vscode-python-environments",
3135
"repository": {
@@ -515,15 +519,16 @@
515519
],
516520
"languageModelTools": [
517521
{
518-
"name": "python_environment",
519-
"userDescription": "%python.languageModelTools.python_environment.userDescription%",
520-
"displayName": "Get Python Environment Information",
521-
"modelDescription": "Provides details about the Python environment for a specified file or workspace, including environment type, Python version, run command, and installed packages with their versions. Use this tool to determine the correct command for executing Python code in this workspace.",
522-
"toolReferenceName": "pythonGetEnvironmentInfo",
522+
"name": "get_python_environment_info",
523+
"displayName": "%python.languageModelTools.get_python_environment_info.displayName%",
524+
"userDescription": "%python.languageModelTools.get_python_environment_info.userDescription%",
525+
"modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ",
526+
"toolReferenceName": "pythonEnvironmentDetails",
523527
"tags": [
524-
"ms-python.python"
528+
"python",
529+
"extension_installed_by_tool"
525530
],
526-
"icon": "$(files)",
531+
"icon": "$(snake)",
527532
"canBeReferencedInPrompt": true,
528533
"inputSchema": {
529534
"type": "object",
@@ -533,19 +538,42 @@
533538
}
534539
},
535540
"description": "The path to the Python file or workspace to get the environment information for.",
536-
"required": [
537-
"resourcePath"
538-
]
541+
"required": []
542+
}
543+
},
544+
{
545+
"name": "get_python_executable_info",
546+
"displayName": "%python.languageModelTools.get_python_executable.displayName%",
547+
"userDescription": "%python.languageModelTools.get_python_executable.userDescription%",
548+
"modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n <env_name> -c 'import sys; print(sys.executable)'`.",
549+
"toolReferenceName": "pythonExecutableDetails",
550+
"tags": [
551+
"python",
552+
"extension_installed_by_tool"
553+
],
554+
"icon": "$(files)",
555+
"canBeReferencedInPrompt": true,
556+
"inputSchema": {
557+
"type": "object",
558+
"properties": {
559+
"resourcePath": {
560+
"type": "string"
561+
}
562+
},
563+
"description": "The path to the Python file or workspace to get the executable information for. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.",
564+
"required": []
539565
}
540566
},
541567
{
542-
"name": "python_install_package",
543-
"displayName": "Install Python Package",
544-
"userDescription": "%python.languageModelTools.python_install_package.userDescription%",
568+
"name": "install_python_package",
569+
"displayName": "%python.languageModelTools.install_python_package.displayName%",
570+
"userDescription": "%python.languageModelTools.install_python_package.userDescription%",
545571
"modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment.",
546-
"toolReferenceName": "pythonInstallPackage",
572+
"toolReferenceName": "installPythonPackage",
547573
"tags": [
548-
"ms-python.python"
574+
"python",
575+
"install python package",
576+
"extension_installed_by_tool"
549577
],
550578
"icon": "$(package)",
551579
"canBeReferencedInPrompt": true,
@@ -561,14 +589,38 @@
561589
},
562590
"resourcePath": {
563591
"type": "string",
564-
"description": "The path to the Python file or workspace to get the environment information for."
592+
"description": "The path to the Python file or workspace into which the packages are installed. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace."
565593
}
566594
},
567595
"required": [
568-
"packageList",
569-
"resourcePath"
596+
"packageList"
570597
]
571598
}
599+
},
600+
{
601+
"name": "create_quick_virtual_environment",
602+
"displayName": "Create a Virtual Environment",
603+
"modelDescription": "This tool will create a Virual Environment",
604+
"tags": [],
605+
"canBeReferencedInPrompt": false,
606+
"inputSchema": {
607+
"type": "object",
608+
"properties": {
609+
"packageList": {
610+
"type": "array",
611+
"items": {
612+
"type": "string"
613+
},
614+
"description": "The list of packages to install."
615+
},
616+
"resourcePath": {
617+
"type": "string",
618+
"description": "The path to the Python file or workspace for which a Python Environment needs to be configured."
619+
}
620+
},
621+
"required": []
622+
},
623+
"when": "false"
572624
}
573625
]
574626
},

package.nls.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
"python-envs.terminal.activate.title": "Activate Environment in Current Terminal",
3636
"python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal",
3737
"python-envs.uninstallPackage.title": "Uninstall Package",
38-
"python.languageModelTools.python_environment.userDescription": "Get Python environment info for a file or path, including version, packages, and the command to run it.",
39-
"python.languageModelTools.python_install_package.userDescription": "Installs Python packages in the given workspace."
38+
"python.languageModelTools.get_python_environment_info.displayName": "Get Python Environment Info",
39+
"python.languageModelTools.get_python_environment_info.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.",
40+
"python.languageModelTools.install_python_package.displayName": "Install Python Package",
41+
"python.languageModelTools.install_python_package.userDescription": "Installs Python packages in a Python Environment.",
42+
"python.languageModelTools.get_python_executable.displayName": "Get Python Executable Info",
43+
"python.languageModelTools.get_python_executable.userDescription": "Get executable info for a Python Environment"
4044
}

src/common/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from 'path';
22

33
export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs';
44
export const PYTHON_EXTENSION_ID = 'ms-python.python';
5+
export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
56
export const EXTENSION_ROOT_DIR = path.dirname(__dirname);
67

78
export const DEFAULT_PACKAGE_MANAGER_ID = 'ms-python.python:pip';
@@ -27,3 +28,4 @@ export const KNOWN_FILES = [
2728
export const KNOWN_TEMPLATE_ENDINGS = ['.j2', '.jinja2'];
2829

2930
export const NEW_PROJECT_TEMPLATES_FOLDER = path.join(EXTENSION_ROOT_DIR, 'src', 'features', 'creators', 'templates');
31+
export const NotebookCellScheme = 'vscode-notebook-cell';

src/extension.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
onDidChangeTerminalShellIntegration,
1717
} from './common/window.apis';
1818
import { createManagerReady } from './features/common/managerReady';
19-
import { GetEnvironmentInfoTool, InstallPackageTool } from './features/copilotTools';
2019
import { AutoFindProjects } from './features/creators/autoFindProjects';
2120
import { ExistingProjects } from './features/creators/existingProjects';
2221
import { NewPackageProject } from './features/creators/newPackageProject';
@@ -69,6 +68,12 @@ import { createNativePythonFinder, NativePythonFinder } from './managers/common/
6968
import { registerCondaFeatures } from './managers/conda/main';
7069
import { registerPoetryFeatures } from './managers/poetry/main';
7170
import { registerPyenvFeatures } from './managers/pyenv/main';
71+
import { GetEnvironmentInfoTool } from './features/chat/getEnvInfoTool';
72+
import { GetExecutableTool } from './features/chat/getExecutableTool';
73+
import { InstallPackageTool } from './features/chat/installPackagesTool';
74+
import { CreateQuickVirtualEnvironmentTool } from './features/chat/createQuickVenvTool';
75+
import { createDeferred } from './common/utils/deferred';
76+
import { SysPythonManager } from './managers/builtin/sysPythonManager';
7277

7378
export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi> {
7479
const start = new StopWatch();
@@ -125,7 +130,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
125130

126131
setPythonApi(envManagers, projectManager, projectCreators, terminalManager, envVarManager);
127132
const api = await getPythonApi();
128-
133+
const sysPythonManager = createDeferred<SysPythonManager>();
129134
const managerView = new EnvManagerView(envManagers);
130135
context.subscriptions.push(managerView);
131136

@@ -143,8 +148,19 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
143148
context.subscriptions.push(
144149
shellStartupVarsMgr,
145150
registerCompletionProvider(envManagers),
146-
registerTools('python_environment', new GetEnvironmentInfoTool(api, envManagers)),
147-
registerTools('python_install_package', new InstallPackageTool(api)),
151+
registerTools(
152+
CreateQuickVirtualEnvironmentTool.toolName,
153+
new CreateQuickVirtualEnvironmentTool(
154+
api,
155+
envManagers,
156+
projectManager,
157+
sysPythonManager.promise,
158+
outputChannel,
159+
),
160+
),
161+
registerTools(GetEnvironmentInfoTool.toolName, new GetEnvironmentInfoTool(api, envManagers)),
162+
registerTools(GetExecutableTool.toolName, new GetExecutableTool(api, envManagers)),
163+
registerTools(InstallPackageTool.toolName, new InstallPackageTool(api)),
148164
commands.registerCommand('python-envs.terminal.revertStartupScriptChanges', async () => {
149165
await cleanupStartupScripts(shellStartupProviders);
150166
}),
@@ -314,8 +330,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
314330
// This is the finder that is used by all the built in environment managers
315331
const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context);
316332
context.subscriptions.push(nativeFinder);
333+
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
334+
sysPythonManager.resolve(sysMgr);
317335
await Promise.all([
318-
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel),
336+
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
319337
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
320338
registerPyenvFeatures(nativeFinder, context.subscriptions),
321339
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel),
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
CancellationToken,
6+
l10n,
7+
LanguageModelTextPart,
8+
LanguageModelTool,
9+
LanguageModelToolInvocationOptions,
10+
LanguageModelToolInvocationPrepareOptions,
11+
LanguageModelToolResult,
12+
LogOutputChannel,
13+
PreparedToolInvocation,
14+
} from 'vscode';
15+
import { PythonEnvironmentApi } from '../../api';
16+
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
17+
import { createAnyEnvironmentCommand } from '../envCommands';
18+
import { getEnvironmentDetails, resolveFilePath } from './utils';
19+
import { SysPythonManager } from '../../managers/builtin/sysPythonManager';
20+
import { ensureGlobalEnv } from '../../managers/builtin/venvUtils';
21+
22+
export interface IResourceReference {
23+
packageList?: string[];
24+
resourcePath?: string;
25+
}
26+
27+
export class CreateQuickVirtualEnvironmentTool implements LanguageModelTool<IResourceReference> {
28+
public static readonly toolName = 'create_quick_virtual_environment';
29+
constructor(
30+
private readonly api: PythonEnvironmentApi,
31+
private readonly envManagers: EnvironmentManagers,
32+
private readonly projectManager: PythonProjectManager,
33+
private readonly sysManager: Promise<SysPythonManager>,
34+
private readonly log: LogOutputChannel,
35+
) {}
36+
async invoke(
37+
options: LanguageModelToolInvocationOptions<IResourceReference>,
38+
token: CancellationToken,
39+
): Promise<LanguageModelToolResult | undefined> {
40+
const resourcePath = resolveFilePath(options.input.resourcePath);
41+
const env = await createAnyEnvironmentCommand(this.envManagers, this.projectManager, {
42+
selectEnvironment: true,
43+
quickCreate: true,
44+
uri: resourcePath,
45+
additionalPackages:
46+
Array.isArray(options.input.packageList) && options.input.packageList.length
47+
? options.input.packageList
48+
: [],
49+
});
50+
if (env) {
51+
const message = await getEnvironmentDetails(
52+
resourcePath,
53+
env,
54+
this.api,
55+
this.envManagers,
56+
undefined,
57+
token,
58+
);
59+
return new LanguageModelToolResult([new LanguageModelTextPart(message)]);
60+
}
61+
}
62+
63+
async prepareInvocation?(
64+
_options: LanguageModelToolInvocationPrepareOptions<IResourceReference>,
65+
_token: CancellationToken,
66+
): Promise<PreparedToolInvocation> {
67+
let version = '';
68+
try {
69+
const sysMgr = await this.sysManager;
70+
const globals = await sysMgr.getEnvironments('global');
71+
const sortedEnvs = ensureGlobalEnv(globals, this.log);
72+
version = getDisplayVersion(sortedEnvs[0].version);
73+
} catch (ex) {
74+
this.log.error('Failed to get Python version for quick virtual environment creation', ex);
75+
}
76+
77+
return {
78+
confirmationMessages: {
79+
title: l10n.t('Create a Virtual Environment{0}?', version ? ` (${version})` : ''),
80+
message: l10n.t(`Virtual Environments provide the benefit of package isolation and more.`),
81+
},
82+
invocationMessage: l10n.t('Creating a Virtual Environment'),
83+
};
84+
}
85+
}
86+
87+
function getDisplayVersion(version: string): string {
88+
if (!version) {
89+
return '';
90+
}
91+
const parts = version.split('.');
92+
if (parts.length < 3) {
93+
return version;
94+
}
95+
return `${parts[0]}.${parts[1]}.${parts[2]}`;
96+
}

src/features/chat/getEnvInfoTool.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
CancellationToken,
6+
l10n,
7+
LanguageModelTextPart,
8+
LanguageModelTool,
9+
LanguageModelToolInvocationOptions,
10+
LanguageModelToolInvocationPrepareOptions,
11+
LanguageModelToolResult,
12+
PreparedToolInvocation,
13+
} from 'vscode';
14+
import { PythonEnvironmentApi } from '../../api';
15+
import { EnvironmentManagers } from '../../internal.api';
16+
import { getPythonPackagesResponse } from './listPackagesTool';
17+
import { getEnvironmentDetails, getToolResponseIfNotebook, raceCancellationError, resolveFilePath } from './utils';
18+
19+
export interface IResourceReference {
20+
resourcePath?: string;
21+
}
22+
23+
export class GetEnvironmentInfoTool implements LanguageModelTool<IResourceReference> {
24+
public static readonly toolName = 'get_python_environment_info';
25+
constructor(private readonly api: PythonEnvironmentApi, private readonly envManagers: EnvironmentManagers) {}
26+
async invoke(
27+
options: LanguageModelToolInvocationOptions<IResourceReference>,
28+
token: CancellationToken,
29+
): Promise<LanguageModelToolResult> {
30+
const resourcePath = resolveFilePath(options.input.resourcePath);
31+
const notebookResponse = getToolResponseIfNotebook(resourcePath);
32+
if (notebookResponse) {
33+
return notebookResponse;
34+
}
35+
const environment = await raceCancellationError(this.api.getEnvironment(resourcePath), token);
36+
if (!environment) {
37+
throw new Error(`No environment found for the provided resource path ${resourcePath?.fsPath}`);
38+
}
39+
40+
const packages = await getPythonPackagesResponse(environment, this.api, token);
41+
const message = await getEnvironmentDetails(
42+
resourcePath,
43+
undefined,
44+
this.api,
45+
this.envManagers,
46+
packages,
47+
token,
48+
);
49+
50+
return new LanguageModelToolResult([new LanguageModelTextPart(message)]);
51+
}
52+
53+
async prepareInvocation?(
54+
_options: LanguageModelToolInvocationPrepareOptions<IResourceReference>,
55+
_token: CancellationToken,
56+
): Promise<PreparedToolInvocation> {
57+
return {
58+
invocationMessage: l10n.t('Fetching Python environment information'),
59+
};
60+
}
61+
}

0 commit comments

Comments
 (0)