Skip to content

Commit 08eeef7

Browse files
authored
Assistant: move installPythonPackage tool to python extension (#10869)
### Summary - addresses #8720 - approach: - moved the tool to the python extension - tag it with `requires-session:python` so it only shows up when a Python session is active (though it actually uses the Terminal to install the packages, not the Console) - also tag it with `requires-actions` so it continues to not be available in Ask mode - there is an upstream version of this tool, but we don't yet support it (see #8826) ### Release Notes #### Bug Fixes - Assistant: disable installPythonPackage tool when in an R context (#8720) ### QA Notes - installPythonPackage tool functionality should remain the same - installPythonPackage should not be available in Ask mode, but should be available in all other chat modes - installPythonPackage should not be available when a Python session is not attached to the chat, e.g. if an R session is attached or if no Console sessions are attached
1 parent fe03b8c commit 08eeef7

File tree

9 files changed

+135
-89
lines changed

9 files changed

+135
-89
lines changed

extensions/positron-assistant/package.json

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -816,31 +816,6 @@
816816
"positron-assistant"
817817
]
818818
},
819-
{
820-
"name": "installPythonPackage",
821-
"displayName": "Install Python Package",
822-
"modelDescription": "Install Python packages using pip. Provide an array of package names to install.",
823-
"icon": "$(package)",
824-
"toolReferenceName": "installPythonPackage",
825-
"userDescription": "Install Python packages in the current environment",
826-
"canBeReferencedInPrompt": true,
827-
"tags": [
828-
"positron-assistant"
829-
],
830-
"inputSchema": {
831-
"type": "object",
832-
"properties": {
833-
"packages": {
834-
"type": "array",
835-
"description": "Array of Python package names to install.",
836-
"items": {
837-
"type": "string",
838-
"description": "Name of the Python package to install."
839-
}
840-
}
841-
}
842-
}
843-
},
844819
{
845820
"name": "runNotebookCells",
846821
"displayName": "Run Notebook Cells",

extensions/positron-assistant/src/api.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
88
import * as positron from 'positron';
99
import { isStreamingEditsEnabled, ParticipantID } from './participants.js';
1010
import { hasAttachedNotebookContext, getAttachedNotebookContext, SerializedNotebookContext } from './tools/notebookUtils.js';
11-
import { MARKDOWN_DIR, TOOL_TAG_REQUIRES_ACTIVE_SESSION, TOOL_TAG_REQUIRES_WORKSPACE, TOOL_TAG_REQUIRES_NOTEBOOK } from './constants.js';
11+
import { MARKDOWN_DIR, TOOL_TAG_REQUIRES_ACTIVE_SESSION, TOOL_TAG_REQUIRES_WORKSPACE, TOOL_TAG_REQUIRES_NOTEBOOK, TOOL_TAG_REQUIRES_ACTIONS } from './constants.js';
1212
import { isWorkspaceOpen } from './utils.js';
1313
import { PositronAssistantToolName } from './types.js';
1414
import path = require('path');
@@ -238,6 +238,11 @@ export function getEnabledTools(
238238
continue;
239239
}
240240

241+
// If the tool requires actions, skip it in Ask mode.
242+
if (tool.tags.includes(TOOL_TAG_REQUIRES_ACTIONS) && isAskMode) {
243+
continue;
244+
}
245+
241246
// If the tool is designed for Positron Assistant but we don't have a
242247
// Positron assistant ID, skip it.
243248
if (tool.name.startsWith('positron') && positronParticipantId === undefined) {
@@ -320,12 +325,6 @@ export function getEnabledTools(
320325
continue;
321326
}
322327
break;
323-
// Only include the installPythonPackage tool when NOT in Ask mode.
324-
case PositronAssistantToolName.InstallPythonPackage:
325-
if (isAskMode) {
326-
continue;
327-
}
328-
break;
329328
// This tool is used by Copilot to edit files; Positron Assistant
330329
// has its own file editing tool. Don't include this tool for
331330
// Positron participants.

extensions/positron-assistant/src/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ export const TOOL_TAG_REQUIRES_ACTIVE_SESSION = 'requires-session';
4848
*/
4949
export const TOOL_TAG_REQUIRES_NOTEBOOK = 'requires-notebook';
5050

51+
/**
52+
* Tag used by tools to indicate that actions will be performed as part of the tool invocation.
53+
* Actions may include code execution in the Console or Terminal, file system modifications, or
54+
* other changes to the user's environment that shouldn't be available in Ask mode.
55+
*
56+
* Tools with this tag will be filtered out of Ask mode sessions to prevent unintended actions.
57+
*/
58+
export const TOOL_TAG_REQUIRES_ACTIONS = 'requires-actions';
59+
5160
/** Max number of variables to include in language session context */
5261
export const MAX_CONTEXT_VARIABLES = 400;
5362

extensions/positron-assistant/src/tools.ts

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -352,62 +352,6 @@ export function registerAssistantTools(
352352
});
353353
context.subscriptions.push(getTableSummaryTool);
354354

355-
const installPythonPackageTool = vscode.lm.registerTool<{
356-
packages: string[];
357-
}>(PositronAssistantToolName.InstallPythonPackage, {
358-
prepareInvocation: async (options, _token) => {
359-
const packageNames = options.input.packages.join(', ');
360-
const result: vscode.PreparedToolInvocation = {
361-
// Display a generic command description rather than a specific pip command
362-
// The actual implementation uses environment-aware package management (pip, conda, poetry, etc.)
363-
// via the Python extension's installPackages command, not direct pip execution
364-
invocationMessage: `Install Python packages: ${packageNames}`,
365-
confirmationMessages: {
366-
title: vscode.l10n.t('Install Python Packages'),
367-
message: options.input.packages.length === 1
368-
? vscode.l10n.t('Positron Assistant wants to install the package {0}. Is this okay?', packageNames)
369-
: vscode.l10n.t('Positron Assistant wants to install the following packages: {0}. Is this okay?', packageNames)
370-
},
371-
};
372-
return result;
373-
},
374-
invoke: async (options, _token) => {
375-
try {
376-
// Use command-based communication - no API leakage
377-
const results = await vscode.commands.executeCommand(
378-
'python.installPackages',
379-
options.input.packages,
380-
{ requireConfirmation: false } // Chat handles confirmations
381-
);
382-
383-
return new vscode.LanguageModelToolResult([
384-
new vscode.LanguageModelTextPart(Array.isArray(results) ? results.join('\n') : String(results))
385-
]);
386-
} catch (error) {
387-
const errorMessage = error instanceof Error ? error.message : String(error);
388-
389-
// Parse error code prefixes from Python extension's installPackages command
390-
// Expected prefixes: [NO_INSTALLER], [VALIDATION_ERROR]
391-
// See: installPackages.ts JSDoc for complete error code documentation
392-
let assistantGuidance = '';
393-
394-
if (errorMessage.startsWith('[NO_INSTALLER]')) {
395-
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.';
396-
} else if (errorMessage.startsWith('[VALIDATION_ERROR]')) {
397-
assistantGuidance = '\n\nSuggestion: Check that the package names are correct and properly formatted.';
398-
} else {
399-
// Fallback for unexpected errors (network issues, permissions, etc.)
400-
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.';
401-
}
402-
403-
return new vscode.LanguageModelToolResult([
404-
new vscode.LanguageModelTextPart(`Package installation encountered an issue: ${errorMessage}${assistantGuidance}`)
405-
]);
406-
}
407-
}
408-
});
409-
410-
context.subscriptions.push(installPythonPackageTool);
411355

412356
context.subscriptions.push(ProjectTreeTool);
413357

extensions/positron-assistant/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export enum PositronAssistantToolName {
1212
ExecuteCode = 'executeCode',
1313
GetTableSummary = 'getTableSummary',
1414
GetPlot = 'getPlot',
15-
InstallPythonPackage = 'installPythonPackage',
1615
InspectVariables = 'inspectVariables',
1716
SelectionEdit = 'selectionEdit',
1817
ProjectTree = 'getProjectTree',

extensions/positron-python/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,6 +1910,33 @@
19101910
}
19111911
],
19121912
"languageModelTools": [
1913+
{
1914+
"name": "installPythonPackage",
1915+
"displayName": "Install Python Package",
1916+
"modelDescription": "Install Python packages using pip. Provide an array of package names to install.",
1917+
"icon": "$(package)",
1918+
"toolReferenceName": "installPythonPackage",
1919+
"userDescription": "%python.languageModelTools.installPythonPackage.userDescription%",
1920+
"canBeReferencedInPrompt": true,
1921+
"tags": [
1922+
"positron-assistant",
1923+
"requires-session:python",
1924+
"requires-actions"
1925+
],
1926+
"inputSchema": {
1927+
"type": "object",
1928+
"properties": {
1929+
"packages": {
1930+
"type": "array",
1931+
"description": "Array of Python package names to install.",
1932+
"items": {
1933+
"type": "string",
1934+
"description": "Name of the Python package to install."
1935+
}
1936+
}
1937+
}
1938+
}
1939+
},
19131940
{
19141941
"name": "get_python_environment_details",
19151942
"displayName": "Get Python Environment Info",

extensions/positron-python/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"displayName": "Python",
33
"description": "Python kernel, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.",
44
"python.command.python.startTerminalREPL.title": "Start Terminal REPL",
5+
"python.languageModelTools.installPythonPackage.userDescription": "Install Python packages in the current environment",
56
"python.languageModelTools.get_python_environment_details.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.",
67
"python.languageModelTools.install_python_packages.userDescription": "Installs Python packages in a Python Environment.",
78
"python.languageModelTools.get_python_executable_details.userDescription": "Get executable info for a Python Environment",

extensions/positron-python/src/client/chat/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import { ConfigurePythonEnvTool } from './configurePythonEnvTool';
1414
import { SelectPythonEnvTool } from './selectEnvTool';
1515
import { CreateVirtualEnvTool } from './createVirtualEnvTool';
1616

17+
// --- Start Positron ---
18+
import { PositronInstallPackagesTool } from './positron/installPackagesTool';
19+
// --- End Positron ---
20+
1721
export function registerTools(
1822
context: IExtensionContext,
1923
discoverApi: IDiscoveryAPI,
@@ -49,4 +53,9 @@ export function registerTools(
4953
new ConfigurePythonEnvTool(environmentsApi, serviceContainer, createVirtualEnvTool),
5054
),
5155
);
56+
57+
// --- Start Positron ---
58+
// Add the positron install packages tool
59+
ourTools.add(lm.registerTool(PositronInstallPackagesTool.toolName, new PositronInstallPackagesTool()));
60+
// --- End Positron ---
5261
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
8+
export interface IPositronInstallPackageArgs {
9+
packages: string[];
10+
}
11+
12+
export class PositronInstallPackagesTool implements vscode.LanguageModelTool<IPositronInstallPackageArgs> {
13+
public static readonly toolName = 'installPythonPackage';
14+
15+
async prepareInvocation(
16+
options: vscode.LanguageModelToolInvocationPrepareOptions<IPositronInstallPackageArgs>,
17+
_token: vscode.CancellationToken,
18+
): Promise<vscode.PreparedToolInvocation> {
19+
const packageNames = options.input.packages.join(', ');
20+
const result: vscode.PreparedToolInvocation = {
21+
// Display a generic command description rather than a specific pip command
22+
// The actual implementation uses environment-aware package management (pip, conda, poetry, etc.)
23+
// via the Python extension's installPackages command, not direct pip execution
24+
invocationMessage: `Install Python packages: ${packageNames}`,
25+
confirmationMessages: {
26+
title: vscode.l10n.t('Install Python Packages'),
27+
message:
28+
options.input.packages.length === 1
29+
? vscode.l10n.t(
30+
'Positron Assistant wants to install the package {0}. Is this okay?',
31+
packageNames,
32+
)
33+
: vscode.l10n.t(
34+
'Positron Assistant wants to install the following packages: {0}. Is this okay?',
35+
packageNames,
36+
),
37+
},
38+
};
39+
return result;
40+
}
41+
42+
async invoke(
43+
options: vscode.LanguageModelToolInvocationOptions<IPositronInstallPackageArgs>,
44+
_token: vscode.CancellationToken,
45+
): Promise<vscode.LanguageModelToolResult> {
46+
try {
47+
// Use command-based communication - no API leakage
48+
const results = await vscode.commands.executeCommand(
49+
'python.installPackages',
50+
options.input.packages,
51+
{ requireConfirmation: false }, // Chat handles confirmations
52+
);
53+
54+
return new vscode.LanguageModelToolResult([
55+
new vscode.LanguageModelTextPart(Array.isArray(results) ? results.join('\n') : String(results)),
56+
]);
57+
} catch (error) {
58+
const errorMessage = error instanceof Error ? error.message : String(error);
59+
60+
// Parse error code prefixes from Python extension's installPackages command
61+
// Expected prefixes: [NO_INSTALLER], [VALIDATION_ERROR]
62+
// See: installPackages.ts JSDoc for complete error code documentation
63+
let assistantGuidance = '';
64+
65+
if (errorMessage.startsWith('[NO_INSTALLER]')) {
66+
assistantGuidance =
67+
'\n\nSuggestion: The Python environment may not be properly configured. Ask the user to check their Python interpreter selection or create a new environment.';
68+
} else if (errorMessage.startsWith('[VALIDATION_ERROR]')) {
69+
assistantGuidance = '\n\nSuggestion: Check that the package names are correct and properly formatted.';
70+
} else {
71+
// Fallback for unexpected errors (network issues, permissions, etc.)
72+
assistantGuidance =
73+
'\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.';
74+
}
75+
76+
return new vscode.LanguageModelToolResult([
77+
new vscode.LanguageModelTextPart(
78+
`Package installation encountered an issue: ${errorMessage}${assistantGuidance}`,
79+
),
80+
]);
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)