diff --git a/extensions/positron-assistant/package.json b/extensions/positron-assistant/package.json
index 10db4a02bd2f..f614d9a6aa75 100644
--- a/extensions/positron-assistant/package.json
+++ b/extensions/positron-assistant/package.json
@@ -360,6 +360,28 @@
}
}
}
+ },
+ {
+ "name": "installPythonPackage",
+ "displayName": "Install Python Package",
+ "modelDescription": "Install Python packages using pip. Provide an array of package names to install.",
+ "canBeReferencedInPrompt": false,
+ "tags": [
+ "positron-assistant"
+ ],
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "packages": {
+ "type": "array",
+ "description": "Array of Python package names to install.",
+ "items": {
+ "type": "string",
+ "description": "Name of the Python package to install."
+ }
+ }
+ }
+ }
}
]
},
diff --git a/extensions/positron-assistant/src/md/prompts/chat/instructions-python.md b/extensions/positron-assistant/src/md/prompts/chat/instructions-python.md
index 552894a30a36..f880d67066e0 100644
--- a/extensions/positron-assistant/src/md/prompts/chat/instructions-python.md
+++ b/extensions/positron-assistant/src/md/prompts/chat/instructions-python.md
@@ -14,6 +14,64 @@ You prefer to show tabular data using the great tables package.
If the USER asks you to use matplotlib or any other Python packages or frameworks, you switch to using these frameworks without mentioning anything else about it. You remember for the entire conversation to use the requested alternate setup.
+
+**Python Package Installation Rules:**
+- NEVER use !pip install commands in Python code blocks
+- NEVER suggest installing packages within Python scripts using ! commands
+- For Python packages that need installation, use the installPythonPackage tool
+- The installPythonPackage tool automatically detects the environment and selects the appropriate installer (pip, conda, uv, poetry, etc.)
+- Only provide import/library code after successful installation
+- Separate installation from code examples
+
+**When to Use installPythonPackage Tool:**
+
+✅ **DO use installPythonPackage when:**
+- User gets `ModuleNotFoundError: No module named 'pandas'`
+- User asks "How do I install matplotlib?"
+- Code requires packages like `numpy`, `scikit-learn`, `plotnine` that aren't in standard library
+- User says "I need to work with data visualization" (likely needs matplotlib/plotnine)
+
+❌ **DON'T use installPythonPackage for:**
+- Standard library modules (`os`, `sys`, `json`, `datetime`, etc.)
+- Built-in functions (`print`, `len`, `range`, etc.)
+
+**Example Workflows:**
+
+**Scenario 1: User asks for data analysis**
+```
+User: "Can you help me analyze some CSV data with pandas?"
+
+Assistant response:
+1. First use installPythonPackage tool with ["pandas"]
+2. Wait for installation success
+3. Then provide code:
+ ```python
+ import pandas as pd
+ df = pd.read_csv('your_file.csv')
+ ```
+```
+
+**Scenario 2: Import error occurs**
+```
+User: "I'm getting ModuleNotFoundError for seaborn"
+
+Assistant response:
+1. Use installPythonPackage tool with ["seaborn"]
+2. Confirm installation succeeded
+3. Suggest re-running the import
+```
+
+**Scenario 3: Multiple packages needed**
+```
+User: "I want to create a machine learning model"
+
+Assistant response:
+1. Use installPythonPackage tool with ["scikit-learn", "pandas", "numpy"]
+2. Wait for all installations
+3. Provide ML code using all packages
+```
+
+
# Create a base DataFrame
diff --git a/extensions/positron-assistant/src/tools.ts b/extensions/positron-assistant/src/tools.ts
index 22785eecfba7..edd78a8d3277 100644
--- a/extensions/positron-assistant/src/tools.ts
+++ b/extensions/positron-assistant/src/tools.ts
@@ -283,6 +283,64 @@ export function registerAssistantTools(
context.subscriptions.push(inspectVariablesTool);
+ const installPythonPackageTool = vscode.lm.registerTool<{
+ packages: string[];
+ }>(PositronAssistantToolName.InstallPythonPackage, {
+ prepareInvocation2: async (options, _token) => {
+ const packageNames = options.input.packages.join(', ');
+ const result: vscode.PreparedTerminalToolInvocation = {
+ // Display a generic command description rather than a specific pip command
+ // The actual implementation uses environment-aware package management (pip, conda, poetry, etc.)
+ // via the Python extension's installPackages command, not direct pip execution
+ command: `Install Python packages: ${packageNames}`,
+ language: 'text', // Not actually a bash command
+ confirmationMessages: {
+ title: vscode.l10n.t('Install Python Packages'),
+ message: options.input.packages.length === 1
+ ? vscode.l10n.t('Positron Assistant wants to install the package {0}. Is this okay?', packageNames)
+ : vscode.l10n.t('Positron Assistant wants to install the following packages: {0}. Is this okay?', packageNames)
+ },
+ };
+ return result;
+ },
+ invoke: async (options, _token) => {
+ try {
+ // Use command-based communication - no API leakage
+ const results = await vscode.commands.executeCommand(
+ 'python.installPackages',
+ options.input.packages,
+ { requireConfirmation: false } // Chat handles confirmations
+ );
+
+ return new vscode.LanguageModelToolResult([
+ new vscode.LanguageModelTextPart(Array.isArray(results) ? results.join('\n') : String(results))
+ ]);
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+
+ // Parse error code prefixes from Python extension's installPackages command
+ // Expected prefixes: [NO_INSTALLER], [VALIDATION_ERROR]
+ // See: installPackages.ts JSDoc for complete error code documentation
+ let assistantGuidance = '';
+
+ if (errorMessage.startsWith('[NO_INSTALLER]')) {
+ assistantGuidance = '\n\nSuggestion: The Python environment may not be properly configured. Ask the user to check their Python interpreter selection or create a new environment.';
+ } else if (errorMessage.startsWith('[VALIDATION_ERROR]')) {
+ assistantGuidance = '\n\nSuggestion: Check that the package names are correct and properly formatted.';
+ } else {
+ // Fallback for unexpected errors (network issues, permissions, etc.)
+ assistantGuidance = '\n\nSuggestion: This may be a network, permissions, or environment issue. You can suggest the user retry the installation or try manual installation via terminal.';
+ }
+
+ return new vscode.LanguageModelToolResult([
+ new vscode.LanguageModelTextPart(`Package installation encountered an issue: ${errorMessage}${assistantGuidance}`)
+ ]);
+ }
+ }
+ });
+
+ context.subscriptions.push(installPythonPackageTool);
+
context.subscriptions.push(ProjectTreeTool);
context.subscriptions.push(DocumentCreateTool);
diff --git a/extensions/positron-assistant/src/types.ts b/extensions/positron-assistant/src/types.ts
index 10fce6224763..9e7eac27645a 100644
--- a/extensions/positron-assistant/src/types.ts
+++ b/extensions/positron-assistant/src/types.ts
@@ -8,6 +8,7 @@ export enum PositronAssistantToolName {
EditFile = 'positron_editFile_internal',
ExecuteCode = 'executeCode',
GetPlot = 'getPlot',
+ InstallPythonPackage = 'installPythonPackage',
InspectVariables = 'inspectVariables',
SelectionEdit = 'selectionEdit',
ProjectTree = 'getProjectTree',
diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json
index 7b872a75f2ef..b5a3f5bb7b00 100644
--- a/extensions/positron-python/package.json
+++ b/extensions/positron-python/package.json
@@ -359,6 +359,11 @@
"command": "python.createEnvironment",
"title": "%python.command.python.createEnvironment.title%"
},
+ {
+ "command": "python.installPackages",
+ "title": "Install Python Packages",
+ "category": "Python"
+ },
{
"category": "Python",
"command": "python.createEnvironment-button",
diff --git a/extensions/positron-python/src/client/common/application/commands.ts b/extensions/positron-python/src/client/common/application/commands.ts
index 0ec71ee5adaa..7bd1241a2580 100644
--- a/extensions/positron-python/src/client/common/application/commands.ts
+++ b/extensions/positron-python/src/client/common/application/commands.ts
@@ -111,6 +111,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
[Commands.Exec_In_Console]: [];
[Commands.Focus_Positron_Console]: [];
[Commands.Create_Pyproject_Toml]: [string | undefined];
+ [Commands.InstallPackages]: [string[]];
// --- End Positron ---
[Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri];
[Commands.Tests_CopilotSetup]: [undefined | Uri];
diff --git a/extensions/positron-python/src/client/common/application/commands/installPackages.ts b/extensions/positron-python/src/client/common/application/commands/installPackages.ts
new file mode 100644
index 000000000000..699e110a9a3c
--- /dev/null
+++ b/extensions/positron-python/src/client/common/application/commands/installPackages.ts
@@ -0,0 +1,77 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2025 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { injectable, inject } from 'inversify';
+import { IExtensionSingleActivationService } from '../../../activation/types';
+import { Commands } from '../../constants';
+import { ICommandManager } from '../types';
+import { IDisposableRegistry } from '../../types';
+import { IInstallationChannelManager, ModuleInstallFlags } from '../../installer/types';
+import { Product } from '../../types';
+
+@injectable()
+export class InstallPackagesCommandHandler implements IExtensionSingleActivationService {
+ public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false };
+
+ constructor(
+ @inject(ICommandManager) private readonly commandManager: ICommandManager,
+ @inject(IInstallationChannelManager) private readonly channelManager: IInstallationChannelManager,
+ @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
+ ) {}
+
+ public async activate(): Promise {
+ this.disposables.push(
+ this.commandManager.registerCommand(Commands.InstallPackages, this.installPackages, this),
+ );
+ }
+
+ /**
+ * Installs Python packages using the appropriate package manager for the current environment.
+ * @param packages Array of package names to install
+ * @returns Promise resolving to array of installation result messages
+ * @throws Error with prefixed error codes for structured error handling:
+ * - `[NO_INSTALLER]` - No compatible package installer found for environment
+ * - `[VALIDATION_ERROR]` - Invalid or missing package names provided
+ * - Other errors may be thrown by underlying installation system without prefixes
+ */
+ public async installPackages(packages: string[]): Promise {
+ // Input validation
+ if (!packages || packages.length === 0) {
+ throw new Error('[VALIDATION_ERROR] At least one package name must be provided');
+ }
+
+ const invalidPackages = packages.filter((pkg) => !pkg || typeof pkg !== 'string' || pkg.trim().length === 0);
+ if (invalidPackages.length > 0) {
+ throw new Error('[VALIDATION_ERROR] All package names must be non-empty strings');
+ }
+ const results: string[] = [];
+
+ // Get installer once upfront to avoid repeated calls
+ const installer = await this.channelManager.getInstallationChannel(Product.pip, undefined);
+ if (!installer) {
+ throw new Error('[NO_INSTALLER] No compatible package installer found for current environment');
+ }
+
+ // Process each package individually, continuing on failures
+ // Note: We don't throw on individual package failures because:
+ // 1. The Assistant can intelligently parse mixed success/failure results
+ // 2. Partial success is often valuable (e.g., pandas works even if matplotlib fails)
+ // 3. Detailed per-package feedback helps the Assistant make better decisions
+ for (const packageName of packages) {
+ try {
+ await installer.installModule(packageName, undefined, undefined, ModuleInstallFlags.none, {
+ waitForCompletion: true,
+ });
+ results.push(`${packageName} installed successfully using ${installer.displayName}`);
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ results.push(`${packageName}: Installation failed - ${errorMsg}`);
+ // Continue with next package to provide complete installation report
+ }
+ }
+
+ return results;
+ }
+}
diff --git a/extensions/positron-python/src/client/common/constants.ts b/extensions/positron-python/src/client/common/constants.ts
index 6301f38a4441..eb86b67c97ef 100644
--- a/extensions/positron-python/src/client/common/constants.ts
+++ b/extensions/positron-python/src/client/common/constants.ts
@@ -86,6 +86,7 @@ export namespace Commands {
export const Is_Global_Python = 'python.isGlobalPython';
export const Show_Interpreter_Debug_Info = 'python.interpreters.debugInfo';
export const Create_Pyproject_Toml = 'python.createPyprojectToml';
+ export const InstallPackages = 'python.installPackages';
// --- End Positron ---
export const InstallJupyter = 'python.installJupyter';
export const InstallPython = 'python.installPython';
diff --git a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts
index 6ef1ec2274ba..7206742868e5 100644
--- a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts
+++ b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts
@@ -42,6 +42,14 @@ export abstract class ModuleInstaller implements IModuleInstaller {
public abstract get type(): ModuleInstallerType;
+ // --- Start Positron ---
+ // This is a temporary flag to allow for the installation to wait for
+ // completion. It can be used to override the behavior that the
+ // python.installModulesInTerminal setting typically controls. This is used
+ // when installing packages using the assistant to make sure it knows how
+ // the installation went.
+ private _waitForCompletion?: boolean;
+ // --- End Positron ---
constructor(protected serviceContainer: IServiceContainer) {}
public async installModule(
@@ -53,6 +61,12 @@ export abstract class ModuleInstaller implements IModuleInstaller {
): Promise {
// --- Start Positron ---
const shouldExecuteInTerminal = this.installModulesInTerminal() || !options?.installAsProcess;
+ // Store the waitForCompletion option so that we can use it in the
+ // executeCommand method. This is temporary and is immediately unset
+ // (before any awaits) in the executeCommand method which takes place in
+ // a single call stack, thus it's not at risk for race conditions with
+ // other calls to installModule..
+ this._waitForCompletion = options?.waitForCompletion;
// --- End Positron ---
const name =
typeof productOrModuleName === 'string'
@@ -259,13 +273,20 @@ export abstract class ModuleInstaller implements IModuleInstaller {
.getTerminalService(options);
// --- Start Positron ---
- // When running with the `python.installModulesInTerminal` setting enabled, we want to
- // ensure that the terminal command is fully executed before returning. Otherwise, the
- // calling code of the install will not be able to tell when the installation is complete.
- if (this.installModulesInTerminal()) {
+ // When running with the `python.installModulesInTerminal` setting
+ // enabled or when the `waitForCompletion` option was set to true in
+ // the parent `installModule` call, we want to ensure that the
+ // terminal command is fully executed before returning. Otherwise,
+ // the calling code of the install will not be able to tell when the
+ // installation is complete.
+
+ if (this.installModulesInTerminal() || this._waitForCompletion) {
// Ensure we pass a cancellation token so that we await the full terminal command
// execution before returning.
const cancelToken = token ?? new CancellationTokenSource().token;
+ // Unset the waitForCompletion flag so that future calls are not blocked if not desired.
+ // If a call needs to be blocked this will be set to true again in the installModule method.
+ this._waitForCompletion = undefined;
await terminalService.sendCommand(command, args, token ?? cancelToken);
return;
}
diff --git a/extensions/positron-python/src/client/common/installer/types.ts b/extensions/positron-python/src/client/common/installer/types.ts
index 10d47e173e33..42fbfb27850c 100644
--- a/extensions/positron-python/src/client/common/installer/types.ts
+++ b/extensions/positron-python/src/client/common/installer/types.ts
@@ -90,4 +90,7 @@ export enum ModuleInstallFlags {
export type InstallOptions = {
installAsProcess?: boolean;
+ // --- Start Positron ---
+ waitForCompletion?: boolean;
+ // --- End Positron ---
};
diff --git a/extensions/positron-python/src/client/common/serviceRegistry.ts b/extensions/positron-python/src/client/common/serviceRegistry.ts
index e7a581552df2..b5db96cd2646 100644
--- a/extensions/positron-python/src/client/common/serviceRegistry.ts
+++ b/extensions/positron-python/src/client/common/serviceRegistry.ts
@@ -90,6 +90,7 @@ import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt';
import { isWindows } from './utils/platform';
import { PixiActivationCommandProvider } from './terminal/environmentActivationProviders/pixiActivationProvider';
// --- Start Positron ---
+import { InstallPackagesCommandHandler } from './application/commands/installPackages';
import { registerPositronTypes } from '../positron/serviceRegistry';
// --- End Positron ---
@@ -120,6 +121,12 @@ export function registerTypes(serviceManager: IServiceManager): void {
IExtensionSingleActivationService,
CreatePythonFileCommandHandler,
);
+ // --- Start Positron ---
+ serviceManager.addSingleton(
+ IExtensionSingleActivationService,
+ InstallPackagesCommandHandler,
+ );
+ // --- End Positron ---
serviceManager.addSingleton(ICommandManager, CommandManager);
serviceManager.addSingleton(IContextKeyManager, ContextKeyManager);
serviceManager.addSingleton(IConfigurationService, ConfigurationService);