From 0b0858035ad8437af1265fa2b82ee3144a2c9af7 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 15 May 2025 11:12:16 +1000 Subject: [PATCH 01/10] More specific llm tools --- package.json | 54 ++++++++- package.nls.json | 6 +- src/client/chat/getExecutableTool.ts | 124 ++++++++++++++++++++ src/client/chat/getPythonEnvTool.ts | 2 +- src/client/chat/index.ts | 16 ++- src/client/chat/installPackagesTool.ts | 13 +-- src/client/chat/listPackagesTool.ts | 152 +++++++++++++++++++++++++ src/client/chat/utils.ts | 16 +++ 8 files changed, 365 insertions(+), 18 deletions(-) create mode 100644 src/client/chat/getExecutableTool.ts create mode 100644 src/client/chat/listPackagesTool.ts diff --git a/package.json b/package.json index 1e6b5691cd16..846930a5c41e 100644 --- a/package.json +++ b/package.json @@ -1465,10 +1465,10 @@ ], "languageModelTools": [ { - "name": "python_environment", + "name": "get_python_environment_info", "displayName": "Get Python Environment Information", "userDescription": "%python.languageModelTools.python_environment.userDescription%", - "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.", + "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. this tool gets details about the Python environment including environment type, Python version, command used to execute Python, and a list of all installed packages with their versions. Use this tool to determine the correct command for executing Python in a terminal.", "toolReferenceName": "pythonGetEnvironmentInfo", "tags": [ "ms-python.python" @@ -1487,6 +1487,56 @@ "resourcePath" ] }, + "when": "false" + }, + { + "name": "get_python_executable", + "displayName": "Get Python Executable", + "userDescription": "%python.languageModelTools.get_python_executable.userDescription%", + "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 -c 'import sys; print(sys.executable)'`.", + "toolReferenceName": "pythonExecutableCommand", + "tags": [ + "ms-python.python" + ], + "icon": "$(files)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string" + } + }, + "description": "The path to the Python file or workspace to get the environment information for.", + "required": [ + "resourcePath" + ] + }, + "when": "!pythonEnvExtensionInstalled" + }, + { + "name": "list_python_packages", + "displayName": "List Python Packages", + "userDescription": "%python.languageModelTools.list_python_packages.userDescription%", + "modelDescription": "This tool will retrieve the list of all installed packages installed in a Python Environment for the specified file or workspace. ALWAYS use this tool instead of executing Python command in the terminal to fetch the list of installed packages. WARNING: Packages installed can change over time, hence the list of packages returned by this tool may not be accurate. Use this tool to get the list of installed packages in a Python environment.", + "toolReferenceName": "listPythonPackages", + "tags": [ + "ms-python.python" + ], + "icon": "$(files)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string" + } + }, + "description": "The path to the Python file or workspace to get the environment information for.", + "required": [ + "resourcePath" + ] + }, "when": "!pythonEnvExtensionInstalled" }, { diff --git a/package.nls.json b/package.nls.json index 22e5cf4fd8ad..76612862d029 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,7 +1,9 @@ { "python.command.python.startTerminalREPL.title": "Start Terminal REPL", - "python.languageModelTools.python_environment.userDescription": "Get Python environment info for a file or path, including version, packages, and the command to run it.", - "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in the given workspace.", + "python.languageModelTools.python_environment.userDescription": "Get Python Environment info for a file or path, including version, packages, and the command to run it.", + "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in a Python Environment.", + "python.languageModelTools.get_python_executable.userDescription": "Get executable info for a Python Environment", + "python.languageModelTools.list_python_packages.userDescription": "Get a list of all installed packages in a Python Environment.", "python.command.python.startNativeREPL.title": "Start Native Python REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts new file mode 100644 index 000000000000..dcbc53377c98 --- /dev/null +++ b/src/client/chat/getExecutableTool.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { getEnvDisplayName, isCondaEnv, raceCancellationError } from './utils'; +import { resolveFilePath } from './utils'; +import { traceError } from '../logging'; +import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; + +export interface IResourceReference { + resourcePath: string; +} + +export class GetExecutableTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_executable'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + + try { + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resourcePath.fsPath); + } + const runCommand = await raceCancellationError(this.getTerminalCommand(environment, resourcePath), token); + + const message = [ + `Following is the information about the Python environment:`, + `1. Environment Type: ${environment.environment?.type || 'unknown'}`, + `2. Version: ${environment.version.sysVersion || 'unknown'}`, + '', + `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, + `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, + `Similarly instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, + ]; + return new LanguageModelToolResult([new LanguageModelTextPart(message.join('\n'))]); + } catch (error) { + if (error instanceof CancellationError) { + throw error; + } + traceError('Error while getting environment information', error); + const errorMessage: string = `An error occurred while fetching environment information: ${error}`; + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); + } + } + + private async getTerminalCommand(environment: ResolvedEnvironment, resource: Uri) { + let cmd: { command: string; args: string[] }; + if (isCondaEnv(environment)) { + cmd = + (await this.getCondaRunCommand(environment)) || + (await this.terminalExecutionService.getExecutableInfo(resource)); + } else { + cmd = await this.terminalExecutionService.getExecutableInfo(resource); + } + return this.terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); + } + + private async getCondaRunCommand(environment: ResolvedEnvironment) { + if (!environment.executable.uri) { + return; + } + const conda = await Conda.getConda(); + if (!conda) { + return; + } + const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); + if (!condaEnv) { + return; + } + const cmd = await conda.getRunPythonArgs(condaEnv, true, false); + if (!cmd) { + return; + } + return { command: cmd[0], args: cmd.slice(1) }; + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName + ? l10n.t('Fetching Python executable information for {0}', envName) + : l10n.t('Fetching Python executable information'), + }; + } +} diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 27a3448fbaf6..1fb5fccae10f 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -42,7 +42,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool { + private readonly pythonExecFactory: IPythonExecutionFactory; + private readonly processServiceFactory: IProcessServiceFactory; + public static readonly toolName = 'list_python_packages'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + this.pythonExecFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + } + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + + try { + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment) { + throw new Error('No environment found for the provided resource path: ' + resourcePath.fsPath); + } + + const packages = isCondaEnv(environment) + ? await raceCancellationError( + listCondaPackages( + this.pythonExecFactory, + environment, + resourcePath, + await raceCancellationError(this.processServiceFactory.create(resourcePath), token), + ), + token, + ) + : await raceCancellationError(listPipPackages(this.pythonExecFactory, resourcePath), token); + + if (!packages.length) { + return new LanguageModelToolResult([new LanguageModelTextPart('No packages found')]); + } + + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + packages.forEach((pkg) => { + const [name, version] = pkg; + response.push(version ? `- ${name} (${version})` : `- ${name}`); + }); + return new LanguageModelToolResult([new LanguageModelTextPart(response.join('\n'))]); + } catch (error) { + if (error instanceof CancellationError) { + throw error; + } + return new LanguageModelToolResult([ + new LanguageModelTextPart(`An error occurred while fetching environment information: ${error}`), + ]); + } + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName ? l10n.t('Listing packages in {0}', envName) : l10n.t('Fetching Python environment information'), + }; + } +} + + +async function listPipPackages(execFactory: IPythonExecutionFactory, resource: Uri): Promise<[packageName:string, version:string][]> { + // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) + // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. + const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); + const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); + return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version] ); +} + +async function listCondaPackages( + execFactory: IPythonExecutionFactory, + env: ResolvedEnvironment, + resource: Uri, + processService: IProcessService, +): Promise<[packageName:string, version:string][]> { + const conda = await Conda.getConda(); + if (!conda) { + traceError('Conda is not installed, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + if (!env.executable.uri) { + traceError('Conda environment executable not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); + if (!condaEnv) { + traceError('Conda environment not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); + if (!cmd) { + traceError('Conda list command not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); + if (!output.stdout) { + traceError('Unable to get conda packages, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); + const packages: [string, string][] = []; + content.forEach((l) => { + const parts = l.split(' ').filter((p) => p.length > 0); + if (parts.length >= 3) { + packages.push([parts[0], parts[1]]); + } + }); + return packages; +} diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 05d92337df43..c23caea2eea7 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -2,6 +2,8 @@ // Licensed under the MIT License. import { CancellationError, CancellationToken, Uri } from 'vscode'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; export function resolveFilePath(filepath: string): Uri { // starts with a scheme @@ -25,3 +27,17 @@ export function raceCancellationError(promise: Promise, token: Cancellatio promise.then(resolve, reject).finally(() => ref.dispose()); }); } + +export async function getEnvDisplayName(discovery: IDiscoveryAPI, resource: Uri, api: PythonExtension['environments']) { + try { + const envPath = api.getActiveEnvironmentPath(resource); + const env = await discovery.resolveEnv(envPath.path); + return env?.display || env?.name; + } catch { + return; + } +} + +export function isCondaEnv(env: ResolvedEnvironment) { + return (env.environment?.type || '').toLowerCase() === 'conda'; +} From 2ee1e0308631e1636d335801d68a8ed38a06edc2 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 15 May 2025 11:37:42 +1000 Subject: [PATCH 02/10] Updates --- package.json | 17 ++++++----------- src/client/chat/getExecutableTool.ts | 6 +++--- src/client/chat/getPythonEnvTool.ts | 8 ++++---- src/client/chat/installPackagesTool.ts | 4 ++-- src/client/chat/listPackagesTool.ts | 8 ++++---- src/client/chat/utils.ts | 9 ++++++--- 6 files changed, 25 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 846930a5c41e..b5429dc4f965 100644 --- a/package.json +++ b/package.json @@ -1507,10 +1507,8 @@ "type": "string" } }, - "description": "The path to the Python file or workspace to get the environment information for.", - "required": [ - "resourcePath" - ] + "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.", + "required": [] }, "when": "!pythonEnvExtensionInstalled" }, @@ -1532,10 +1530,8 @@ "type": "string" } }, - "description": "The path to the Python file or workspace to get the environment information for.", - "required": [ - "resourcePath" - ] + "description": "The path to the Python file or workspace to list the packages. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.", + "required": [] }, "when": "!pythonEnvExtensionInstalled" }, @@ -1562,12 +1558,11 @@ }, "resourcePath": { "type": "string", - "description": "The path to the Python file or workspace to get the environment information for." + "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." } }, "required": [ - "packageList", - "resourcePath" + "packageList" ] }, "when": "!pythonEnvExtensionInstalled" diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index dcbc53377c98..b85b6c4976a4 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -25,7 +25,7 @@ import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; export interface IResourceReference { - resourcePath: string; + resourcePath?: string; } export class GetExecutableTool implements LanguageModelTool { @@ -54,7 +54,7 @@ export class GetExecutableTool implements LanguageModelTool const envPath = this.api.getActiveEnvironmentPath(resourcePath); const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resourcePath.fsPath); + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); } const runCommand = await raceCancellationError(this.getTerminalCommand(environment, resourcePath), token); @@ -78,7 +78,7 @@ export class GetExecutableTool implements LanguageModelTool } } - private async getTerminalCommand(environment: ResolvedEnvironment, resource: Uri) { + private async getTerminalCommand(environment: ResolvedEnvironment, resource?: Uri) { let cmd: { command: string; args: string[] }; if (isCondaEnv(environment)) { cmd = diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 1fb5fccae10f..76fe0ff066a8 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -25,7 +25,7 @@ import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; import { traceError } from '../logging'; export interface IResourceReference { - resourcePath: string; + resourcePath?: string; } interface EnvironmentInfo { @@ -79,7 +79,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool(IModuleInstaller); diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts index b1ba8078bfbe..c25f7e215669 100644 --- a/src/client/chat/listPackagesTool.ts +++ b/src/client/chat/listPackagesTool.ts @@ -24,7 +24,7 @@ import { traceError } from '../logging'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; export interface IResourceReference { - resourcePath: string; + resourcePath?: string; } export class ListPythonPackagesTool implements LanguageModelTool { @@ -51,7 +51,7 @@ export class ListPythonPackagesTool implements LanguageModelTool { +async function listPipPackages(execFactory: IPythonExecutionFactory, resource: Uri | undefined): Promise<[packageName:string, version:string][]> { // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); @@ -113,7 +113,7 @@ async function listPipPackages(execFactory: IPythonExecutionFactory, resource: U async function listCondaPackages( execFactory: IPythonExecutionFactory, env: ResolvedEnvironment, - resource: Uri, + resource: Uri | undefined, processService: IProcessService, ): Promise<[packageName:string, version:string][]> { const conda = await Conda.getConda(); diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index c23caea2eea7..b6610d25b6b0 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationError, CancellationToken, Uri } from 'vscode'; +import { CancellationError, CancellationToken, Uri, workspace } from 'vscode'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; import { PythonExtension, ResolvedEnvironment } from '../api/types'; -export function resolveFilePath(filepath: string): Uri { +export function resolveFilePath(filepath?: string): Uri | undefined { + if (!filepath) { + return workspace.workspaceFolders ? workspace.workspaceFolders[0].uri : undefined; + } // starts with a scheme try { return Uri.parse(filepath); @@ -28,7 +31,7 @@ export function raceCancellationError(promise: Promise, token: Cancellatio }); } -export async function getEnvDisplayName(discovery: IDiscoveryAPI, resource: Uri, api: PythonExtension['environments']) { +export async function getEnvDisplayName(discovery: IDiscoveryAPI, resource: Uri | undefined, api: PythonExtension['environments']) { try { const envPath = api.getActiveEnvironmentPath(resource); const env = await discovery.resolveEnv(envPath.path); From 8ceea71235c8d395b25525884d40791720a0fbfd Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 15 May 2025 14:02:04 +1000 Subject: [PATCH 03/10] updates --- src/client/chat/getPythonEnvTool.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 76fe0ff066a8..ed98e94e3bde 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -128,13 +128,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool Date: Thu, 15 May 2025 19:08:54 +1000 Subject: [PATCH 04/10] Updates --- src/client/chat/listPackagesTool.ts | 14 +++++++++----- src/test/common.ts | 2 +- .../pythonPathUpdaterFactory.unit.test.ts | 1 + .../testCancellationRunAdapters.unit.test.ts | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts index c25f7e215669..6c70e4731481 100644 --- a/src/client/chat/listPackagesTool.ts +++ b/src/client/chat/listPackagesTool.ts @@ -96,18 +96,22 @@ export class ListPythonPackagesTool implements LanguageModelTool { +async function listPipPackages( + execFactory: IPythonExecutionFactory, + resource: Uri | undefined, +): Promise<[string, string][]> { // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); - return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version] ); + return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version]); } async function listCondaPackages( @@ -115,7 +119,7 @@ async function listCondaPackages( env: ResolvedEnvironment, resource: Uri | undefined, processService: IProcessService, -): Promise<[packageName:string, version:string][]> { +): Promise<[string, string][]> { const conda = await Conda.getConda(); if (!conda) { traceError('Conda is not installed, falling back to pip packages'); diff --git a/src/test/common.ts b/src/test/common.ts index b6e352b9a3e8..886323e815a5 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -62,7 +62,7 @@ export async function updateSetting( configTarget: ConfigurationTarget, ) { const vscode = require('vscode') as typeof import('vscode'); - const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' } || null); + const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' }); const currentValue = settings.inspect(setting); if ( currentValue !== undefined && diff --git a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts index 762c23d86c8e..5c851b8071f3 100644 --- a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -17,6 +17,7 @@ suite('Python Path Settings Updater', () => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); interpreterPathService = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index ceee7f54f447..57baebb7230e 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -36,6 +36,7 @@ suite('Execution Flow Run Adapters', () => { let useEnvExtensionStub: sinon.SinonStub; setup(() => { + mockProc = {} as MockChildProcess; useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); useEnvExtensionStub.returns(false); // general vars From 6a421671a9be640c72833fa014a05adcbadc983b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 15 May 2025 19:27:36 +1000 Subject: [PATCH 05/10] Updates --- package.json | 14 ++- package.nls.json | 2 +- src/client/chat/getExecutableTool.ts | 69 +++++++------ src/client/chat/getPythonEnvTool.ts | 147 ++++++--------------------- src/client/chat/listPackagesTool.ts | 67 +++++++----- 5 files changed, 118 insertions(+), 181 deletions(-) diff --git a/package.json b/package.json index b5429dc4f965..7c487f4a752f 100644 --- a/package.json +++ b/package.json @@ -1466,9 +1466,9 @@ "languageModelTools": [ { "name": "get_python_environment_info", - "displayName": "Get Python Environment Information", - "userDescription": "%python.languageModelTools.python_environment.userDescription%", - "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. this tool gets details about the Python environment including environment type, Python version, command used to execute Python, and a list of all installed packages with their versions. Use this tool to determine the correct command for executing Python in a terminal.", + "displayName": "Get Python Environment Info", + "userDescription": "%python.languageModelTools.get_python_environment_info.userDescription%", + "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. ", "toolReferenceName": "pythonGetEnvironmentInfo", "tags": [ "ms-python.python" @@ -1486,8 +1486,7 @@ "required": [ "resourcePath" ] - }, - "when": "false" + } }, { "name": "get_python_executable", @@ -1509,8 +1508,7 @@ }, "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.", "required": [] - }, - "when": "!pythonEnvExtensionInstalled" + } }, { "name": "list_python_packages", @@ -1533,7 +1531,7 @@ "description": "The path to the Python file or workspace to list the packages. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.", "required": [] }, - "when": "!pythonEnvExtensionInstalled" + "when": "false" }, { "name": "python_install_package", diff --git a/package.nls.json b/package.nls.json index 76612862d029..a73deb3554da 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,6 +1,6 @@ { "python.command.python.startTerminalREPL.title": "Start Terminal REPL", - "python.languageModelTools.python_environment.userDescription": "Get Python Environment info for a file or path, including version, packages, and the command to run it.", + "python.languageModelTools.get_python_environment_info.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.", "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in a Python Environment.", "python.languageModelTools.get_python_executable.userDescription": "Get executable info for a Python Environment", "python.languageModelTools.list_python_packages.userDescription": "Get a list of all installed packages in a Python Environment.", diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index b85b6c4976a4..11ff02727424 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -56,7 +56,10 @@ export class GetExecutableTool implements LanguageModelTool if (!environment || !environment.version) { throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); } - const runCommand = await raceCancellationError(this.getTerminalCommand(environment, resourcePath), token); + const runCommand = await raceCancellationError( + getTerminalCommand(environment, resourcePath, this.terminalExecutionService, this.terminalHelper), + token, + ); const message = [ `Following is the information about the Python environment:`, @@ -78,37 +81,6 @@ export class GetExecutableTool implements LanguageModelTool } } - private async getTerminalCommand(environment: ResolvedEnvironment, resource?: Uri) { - let cmd: { command: string; args: string[] }; - if (isCondaEnv(environment)) { - cmd = - (await this.getCondaRunCommand(environment)) || - (await this.terminalExecutionService.getExecutableInfo(resource)); - } else { - cmd = await this.terminalExecutionService.getExecutableInfo(resource); - } - return this.terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); - } - - private async getCondaRunCommand(environment: ResolvedEnvironment) { - if (!environment.executable.uri) { - return; - } - const conda = await Conda.getConda(); - if (!conda) { - return; - } - const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); - if (!condaEnv) { - return; - } - const cmd = await conda.getRunPythonArgs(condaEnv, true, false); - if (!cmd) { - return; - } - return { command: cmd[0], args: cmd.slice(1) }; - } - async prepareInvocation?( options: LanguageModelToolInvocationPrepareOptions, token: CancellationToken, @@ -122,3 +94,36 @@ export class GetExecutableTool implements LanguageModelTool }; } } + +export async function getTerminalCommand( + environment: ResolvedEnvironment, + resource: Uri | undefined, + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, +): Promise { + let cmd: { command: string; args: string[] }; + if (isCondaEnv(environment)) { + cmd = (await getCondaRunCommand(environment)) || (await terminalExecutionService.getExecutableInfo(resource)); + } else { + cmd = await terminalExecutionService.getExecutableInfo(resource); + } + return terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); +} +async function getCondaRunCommand(environment: ResolvedEnvironment) { + if (!environment.executable.uri) { + return; + } + const conda = await Conda.getConda(); + if (!conda) { + return; + } + const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); + if (!condaEnv) { + return; + } + const cmd = await conda.getRunPythonArgs(condaEnv, true, false); + if (!cmd) { + return; + } + return { command: cmd[0], args: cmd.slice(1) }; +} diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index ed98e94e3bde..df85422456b2 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -11,37 +11,27 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - Uri, } from 'vscode'; -import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; -import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; import { raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; -import { parsePipList } from './pipListUtils'; -import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; -import { traceError } from '../logging'; +import { getPythonPackagesResponse } from './listPackagesTool'; +import { getTerminalCommand } from './getExecutableTool'; +import { ITerminalHelper } from '../common/terminal/types'; export interface IResourceReference { resourcePath?: string; } -interface EnvironmentInfo { - type: string; // e.g. conda, venv, virtualenv, sys - version: string; - runCommand: string; - packages: string[] | string; //include versions too -} - -/** - * A tool to get the information about the Python environment. - */ export class GetEnvironmentInfoTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly pythonExecFactory: IPythonExecutionFactory; private readonly processServiceFactory: IProcessServiceFactory; + private readonly terminalHelper: ITerminalHelper; public static readonly toolName = 'get_python_environment_info'; constructor( private readonly api: PythonExtension['environments'], @@ -53,6 +43,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool(IPythonExecutionFactory); this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); } /** * Invokes the tool to get the information about the Python environment. @@ -66,14 +57,6 @@ export class GetEnvironmentInfoTool implements LanguageModelTool { const resourcePath = resolveFilePath(options.input.resourcePath); - // environment info set to default values - const envInfo: EnvironmentInfo = { - type: 'no type found', - version: 'no version found', - packages: 'no packages found', - runCommand: 'no run command found', - }; - try { // environment const envPath = this.api.getActiveEnvironmentPath(resourcePath); @@ -81,38 +64,38 @@ export class GetEnvironmentInfoTool implements LanguageModelTool 0 ? `${cmd.command} ${cmd.args.join(' ')}` : executable; - envInfo.version = environment.version.sysVersion; + const [packages, runCommand] = await Promise.all([ + getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, + token, + ), + raceCancellationError( + getTerminalCommand(environment, resourcePath, this.terminalExecutionService, this.terminalHelper), + token, + ), + ]); - const isConda = (environment.environment?.type || '').toLowerCase() === 'conda'; - envInfo.packages = isConda - ? await raceCancellationError( - listCondaPackages( - this.pythonExecFactory, - environment, - resourcePath, - await raceCancellationError(this.processServiceFactory.create(resourcePath), token), - ), - token, - ) - : await raceCancellationError(listPipPackages(this.pythonExecFactory, resourcePath), token); + const message = [ + `Following is the information about the Python environment:`, + `1. Environment Type: ${environment.environment?.type || 'unknown'}`, + `2. Version: ${environment.version.sysVersion || 'unknown'}`, + '', + `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, + `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, + `Similarly, instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, + `4. ${packages}`, + ]; - // format and return - return new LanguageModelToolResult([BuildEnvironmentInfoContent(envInfo)]); + return new LanguageModelToolResult([new LanguageModelTextPart(message.join('\n'))]); } catch (error) { if (error instanceof CancellationError) { throw error; } const errorMessage: string = `An error occurred while fetching environment information: ${error}`; - const partialContent = BuildEnvironmentInfoContent(envInfo); - return new LanguageModelToolResult([ - new LanguageModelTextPart(`${errorMessage}\n\n${partialContent.value}`), - ]); + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); } } @@ -125,69 +108,3 @@ export class GetEnvironmentInfoTool implements LanguageModelTool or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. - "packages": ${JSON.stringify(Array.isArray(envInfo.packages) ? envInfo.packages : envInfo.packages, null, 2)} -}`; - - return new LanguageModelTextPart(content); -} - -async function listPipPackages(execFactory: IPythonExecutionFactory, resource: Uri | undefined) { - // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) - // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. - const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); - const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); - return parsePipList(output.stdout).map((pkg) => (pkg.version ? `${pkg.name} (${pkg.version})` : pkg.name)); -} - -async function listCondaPackages( - execFactory: IPythonExecutionFactory, - env: ResolvedEnvironment, - resource: Uri | undefined, - processService: IProcessService, -) { - const conda = await Conda.getConda(); - if (!conda) { - traceError('Conda is not installed, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - if (!env.executable.uri) { - traceError('Conda environment executable not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); - if (!condaEnv) { - traceError('Conda environment not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); - if (!cmd) { - traceError('Conda list command not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); - if (!output.stdout) { - traceError('Unable to get conda packages, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); - const packages: string[] = []; - content.forEach((l) => { - const parts = l.split(' ').filter((p) => p.length > 0); - if (parts.length === 3) { - packages.push(`${parts[0]} (${parts[1]})`); - } - }); - return packages; -} diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts index 6c70e4731481..157bdcf34793 100644 --- a/src/client/chat/listPackagesTool.ts +++ b/src/client/chat/listPackagesTool.ts @@ -54,31 +54,14 @@ export class ListPythonPackagesTool implements LanguageModelTool or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. - const response = [ - 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', - ]; - packages.forEach((pkg) => { - const [name, version] = pkg; - response.push(version ? `- ${name} (${version})` : `- ${name}`); - }); - return new LanguageModelToolResult([new LanguageModelTextPart(response.join('\n'))]); + const message = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); } catch (error) { if (error instanceof CancellationError) { throw error; @@ -103,6 +86,40 @@ export class ListPythonPackagesTool implements LanguageModelTool { + const packages = isCondaEnv(environment) + ? await raceCancellationError( + listCondaPackages( + pythonExecFactory, + environment, + resourcePath, + await raceCancellationError(processServiceFactory.create(resourcePath), token), + ), + token, + ) + : await raceCancellationError(listPipPackages(pythonExecFactory, resourcePath), token); + + if (!packages.length) { + return 'No packages found'; + } + + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + packages.forEach((pkg) => { + const [name, version] = pkg; + response.push(version ? `- ${name} (${version})` : `- ${name}`); + }); + return response.join('\n'); +} + async function listPipPackages( execFactory: IPythonExecutionFactory, resource: Uri | undefined, From bd14f1a5540c4c7b2b7a9936913ccb49edfbedd7 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 16 May 2025 08:18:11 +1000 Subject: [PATCH 06/10] Updates --- src/client/chat/getExecutableTool.ts | 80 ++++++++-------------------- src/client/chat/getPythonEnvTool.ts | 56 ++++++++++--------- src/client/chat/utils.ts | 74 ++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 84 deletions(-) diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index 11ff02727424..4a86f847255b 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -11,18 +11,17 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - Uri, } from 'vscode'; -import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; -import { getEnvDisplayName, isCondaEnv, raceCancellationError } from './utils'; +import { getEnvDisplayName, getEnvironmentDetails, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { traceError } from '../logging'; -import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; +import { ITerminalHelper } from '../common/terminal/types'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; -import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { ConfigurePythonEnvTool } from './configurePythonEnvTool'; export interface IResourceReference { resourcePath?: string; @@ -47,30 +46,30 @@ export class GetExecutableTool implements LanguageModelTool options: LanguageModelToolInvocationOptions, token: CancellationToken, ): Promise { + if (!ConfigurePythonEnvTool.EnvironmentConfigured) { + return new LanguageModelToolResult([ + new LanguageModelTextPart( + [ + `A Python environment is not configured. Please configure a Python environment first using the ${ConfigurePythonEnvTool.toolName}.`, + `The ${ConfigurePythonEnvTool.toolName} tool will guide the user through the process of configuring a Python environment.`, + 'Once the environment is configured, you can use this tool to get the Python executable information.', + ].join('\n'), + ), + ]); + } + const resourcePath = resolveFilePath(options.input.resourcePath); try { - // environment - const envPath = this.api.getActiveEnvironmentPath(resourcePath); - const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); - if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); - } - const runCommand = await raceCancellationError( - getTerminalCommand(environment, resourcePath, this.terminalExecutionService, this.terminalHelper), + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + undefined, token, ); - - const message = [ - `Following is the information about the Python environment:`, - `1. Environment Type: ${environment.environment?.type || 'unknown'}`, - `2. Version: ${environment.version.sysVersion || 'unknown'}`, - '', - `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, - `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, - `Similarly instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, - ]; - return new LanguageModelToolResult([new LanguageModelTextPart(message.join('\n'))]); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); } catch (error) { if (error instanceof CancellationError) { throw error; @@ -94,36 +93,3 @@ export class GetExecutableTool implements LanguageModelTool }; } } - -export async function getTerminalCommand( - environment: ResolvedEnvironment, - resource: Uri | undefined, - terminalExecutionService: TerminalCodeExecutionProvider, - terminalHelper: ITerminalHelper, -): Promise { - let cmd: { command: string; args: string[] }; - if (isCondaEnv(environment)) { - cmd = (await getCondaRunCommand(environment)) || (await terminalExecutionService.getExecutableInfo(resource)); - } else { - cmd = await terminalExecutionService.getExecutableInfo(resource); - } - return terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); -} -async function getCondaRunCommand(environment: ResolvedEnvironment) { - if (!environment.executable.uri) { - return; - } - const conda = await Conda.getConda(); - if (!conda) { - return; - } - const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); - if (!condaEnv) { - return; - } - const cmd = await conda.getRunPythonArgs(condaEnv, true, false); - if (!cmd) { - return; - } - return { command: cmd[0], args: cmd.slice(1) }; -} diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index df85422456b2..aa527e352f2d 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -17,11 +17,11 @@ import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { raceCancellationError } from './utils'; +import { getEnvironmentDetails, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { getPythonPackagesResponse } from './listPackagesTool'; -import { getTerminalCommand } from './getExecutableTool'; import { ITerminalHelper } from '../common/terminal/types'; +import { ConfigurePythonEnvTool } from './configurePythonEnvTool'; export interface IResourceReference { resourcePath?: string; @@ -55,6 +55,18 @@ export class GetEnvironmentInfoTool implements LanguageModelTool, token: CancellationToken, ): Promise { + if (!ConfigurePythonEnvTool.EnvironmentConfigured) { + return new LanguageModelToolResult([ + new LanguageModelTextPart( + [ + `A Python environment is not configured. Please configure a Python environment first using the ${ConfigurePythonEnvTool.toolName}.`, + `The ${ConfigurePythonEnvTool.toolName} tool will guide the user through the process of configuring a Python environment.`, + 'Once the environment is configured, you can use this tool to get the Python executable information.', + ].join('\n'), + ), + ]); + } + const resourcePath = resolveFilePath(options.input.resourcePath); try { @@ -64,32 +76,24 @@ export class GetEnvironmentInfoTool implements LanguageModelTool(promise: Promise, token: Cancellatio }); } -export async function getEnvDisplayName(discovery: IDiscoveryAPI, resource: Uri | undefined, api: PythonExtension['environments']) { +export async function getEnvDisplayName( + discovery: IDiscoveryAPI, + resource: Uri | undefined, + api: PythonExtension['environments'], +) { try { const envPath = api.getActiveEnvironmentPath(resource); const env = await discovery.resolveEnv(envPath.path); @@ -44,3 +51,68 @@ export async function getEnvDisplayName(discovery: IDiscoveryAPI, resource: Uri export function isCondaEnv(env: ResolvedEnvironment) { return (env.environment?.type || '').toLowerCase() === 'conda'; } + +export async function getEnvironmentDetails( + resourcePath: Uri | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + packages: string | undefined, + token: CancellationToken, +): Promise { + // environment + const envPath = api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + } + const runCommand = await raceCancellationError( + getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), + token, + ); + + const message = [ + `Following is the information about the Python environment:`, + `1. Environment Type: ${environment.environment?.type || 'unknown'}`, + `2. Version: ${environment.version.sysVersion || 'unknown'}`, + '', + `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, + `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, + `Similarly instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, + packages ? `4. ${packages}` : '', + ]; + return message.join('\n'); +} + +export async function getTerminalCommand( + environment: ResolvedEnvironment, + resource: Uri | undefined, + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, +): Promise { + let cmd: { command: string; args: string[] }; + if (isCondaEnv(environment)) { + cmd = (await getCondaRunCommand(environment)) || (await terminalExecutionService.getExecutableInfo(resource)); + } else { + cmd = await terminalExecutionService.getExecutableInfo(resource); + } + return terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); +} +async function getCondaRunCommand(environment: ResolvedEnvironment) { + if (!environment.executable.uri) { + return; + } + const conda = await Conda.getConda(); + if (!conda) { + return; + } + const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); + if (!condaEnv) { + return; + } + const cmd = await conda.getRunPythonArgs(condaEnv, true, false); + if (!cmd) { + return; + } + return { command: cmd[0], args: cmd.slice(1) }; +} From 596245b5b1c5a7669ebe09825edf6a54c3934204 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 16 May 2025 08:23:45 +1000 Subject: [PATCH 07/10] Updates --- src/client/chat/getExecutableTool.ts | 13 ------------- src/client/chat/getPythonEnvTool.ts | 13 ------------- 2 files changed, 26 deletions(-) diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index 4a86f847255b..af4ab214419a 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -21,7 +21,6 @@ import { resolveFilePath } from './utils'; import { traceError } from '../logging'; import { ITerminalHelper } from '../common/terminal/types'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; -import { ConfigurePythonEnvTool } from './configurePythonEnvTool'; export interface IResourceReference { resourcePath?: string; @@ -46,18 +45,6 @@ export class GetExecutableTool implements LanguageModelTool options: LanguageModelToolInvocationOptions, token: CancellationToken, ): Promise { - if (!ConfigurePythonEnvTool.EnvironmentConfigured) { - return new LanguageModelToolResult([ - new LanguageModelTextPart( - [ - `A Python environment is not configured. Please configure a Python environment first using the ${ConfigurePythonEnvTool.toolName}.`, - `The ${ConfigurePythonEnvTool.toolName} tool will guide the user through the process of configuring a Python environment.`, - 'Once the environment is configured, you can use this tool to get the Python executable information.', - ].join('\n'), - ), - ]); - } - const resourcePath = resolveFilePath(options.input.resourcePath); try { diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index aa527e352f2d..ef200239af9a 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -21,7 +21,6 @@ import { getEnvironmentDetails, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { getPythonPackagesResponse } from './listPackagesTool'; import { ITerminalHelper } from '../common/terminal/types'; -import { ConfigurePythonEnvTool } from './configurePythonEnvTool'; export interface IResourceReference { resourcePath?: string; @@ -55,18 +54,6 @@ export class GetEnvironmentInfoTool implements LanguageModelTool, token: CancellationToken, ): Promise { - if (!ConfigurePythonEnvTool.EnvironmentConfigured) { - return new LanguageModelToolResult([ - new LanguageModelTextPart( - [ - `A Python environment is not configured. Please configure a Python environment first using the ${ConfigurePythonEnvTool.toolName}.`, - `The ${ConfigurePythonEnvTool.toolName} tool will guide the user through the process of configuring a Python environment.`, - 'Once the environment is configured, you can use this tool to get the Python executable information.', - ].join('\n'), - ), - ]); - } - const resourcePath = resolveFilePath(options.input.resourcePath); try { From f88d7047f849287283e7021f6bd2c6c374e65cd0 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 16 May 2025 09:35:48 +1000 Subject: [PATCH 08/10] Fix test failures --- .../testCancellationRunAdapters.unit.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index 57baebb7230e..3ad05339ff97 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -16,6 +16,7 @@ import { UnittestTestExecutionAdapter } from '../../../client/testing/testContro import { MockChildProcess } from '../../mocks/mockChildProcess'; import * as util from '../../../client/testing/testController/common/utils'; import * as extapi from '../../../client/envExt/api.internal'; +import { noop } from '../../core'; const adapters: Array = ['pytest', 'unittest']; @@ -36,7 +37,11 @@ suite('Execution Flow Run Adapters', () => { let useEnvExtensionStub: sinon.SinonStub; setup(() => { - mockProc = {} as MockChildProcess; + const proc = typeMoq.Mock.ofType(); + proc.setup((p) => p.on).returns(() => noop as any ); + proc.setup((p) => p.stdout).returns(() => null ); + proc.setup((p) => p.stderr).returns(() => null ); + mockProc = proc.object; useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); useEnvExtensionStub.returns(false); // general vars From abfb605a35a8d85f6568d721b06bfbbf380b7749 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 16 May 2025 09:47:12 +1000 Subject: [PATCH 09/10] Updates --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 56200fd27b76..9d1cf544b692 100644 --- a/package.json +++ b/package.json @@ -1483,9 +1483,7 @@ } }, "description": "The path to the Python file or workspace to get the environment information for.", - "required": [ - "resourcePath" - ] + "required": [] } }, { From a653a6e59e94b0b1facad73f5323677a15ec4da5 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 16 May 2025 09:47:36 +1000 Subject: [PATCH 10/10] updates --- .../testController/testCancellationRunAdapters.unit.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index 3ad05339ff97..cdf0d00c5dc4 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -38,9 +38,9 @@ suite('Execution Flow Run Adapters', () => { setup(() => { const proc = typeMoq.Mock.ofType(); - proc.setup((p) => p.on).returns(() => noop as any ); - proc.setup((p) => p.stdout).returns(() => null ); - proc.setup((p) => p.stderr).returns(() => null ); + proc.setup((p) => p.on).returns(() => noop as any); + proc.setup((p) => p.stdout).returns(() => null); + proc.setup((p) => p.stderr).returns(() => null); mockProc = proc.object; useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); useEnvExtensionStub.returns(false);