Skip to content

Smarter assistant python pkg install #8337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions extensions/positron-assistant/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</style-python>

<python-package-management>
**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
```
</python-package-management>

<examples-python>
# Create a base DataFrame

Expand Down
58 changes: 58 additions & 0 deletions extensions/positron-assistant/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions extensions/positron-assistant/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum PositronAssistantToolName {
EditFile = 'positron_editFile_internal',
ExecuteCode = 'executeCode',
GetPlot = 'getPlot',
InstallPythonPackage = 'installPythonPackage',
InspectVariables = 'inspectVariables',
SelectionEdit = 'selectionEdit',
ProjectTree = 'getProjectTree',
Expand Down
5 changes: 5 additions & 0 deletions extensions/positron-python/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
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<string[]> {
// 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;
}
}
1 change: 1 addition & 0 deletions extensions/positron-python/src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -53,6 +61,12 @@ export abstract class ModuleInstaller implements IModuleInstaller {
): Promise<void> {
// --- 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'
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,7 @@ export enum ModuleInstallFlags {

export type InstallOptions = {
installAsProcess?: boolean;
// --- Start Positron ---
waitForCompletion?: boolean;
// --- End Positron ---
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---

Expand Down Expand Up @@ -120,6 +121,12 @@ export function registerTypes(serviceManager: IServiceManager): void {
IExtensionSingleActivationService,
CreatePythonFileCommandHandler,
);
// --- Start Positron ---
serviceManager.addSingleton<IExtensionSingleActivationService>(
IExtensionSingleActivationService,
InstallPackagesCommandHandler,
);
// --- End Positron ---
serviceManager.addSingleton<ICommandManager>(ICommandManager, CommandManager);
serviceManager.addSingleton<IContextKeyManager>(IContextKeyManager, ContextKeyManager);
serviceManager.addSingleton<IConfigurationService>(IConfigurationService, ConfigurationService);
Expand Down
Loading