Skip to content

Commit 5035e88

Browse files
authored
Configuration tool with improved workflow and UX (#25106)
The new tool `Configure Python` will get invoked before other tools (at least if Model decides to do so) * We prompt the user to create a virtual env with the latest stable version of Python available * If user cancels we prompt the user to select an existing Python env https://github.com/user-attachments/assets/d4bfbadf-fcbf-4a17-b1b6-7670e16c8cf4
1 parent 2db24aa commit 5035e88

16 files changed

+734
-307
lines changed

package.json

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,7 @@
14751475
"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. ALWAYS call configure_python_environment before using this tool.",
14761476
"toolReferenceName": "pythonGetEnvironmentInfo",
14771477
"tags": [
1478+
"extension_installed_by_tool",
14781479
"enable_other_tool_configure_python_environment"
14791480
],
14801481
"icon": "$(snake)",
@@ -1498,6 +1499,7 @@
14981499
"modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n <env_name> -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool.",
14991500
"toolReferenceName": "pythonExecutableCommand",
15001501
"tags": [
1502+
"extension_installed_by_tool",
15011503
"enable_other_tool_configure_python_environment"
15021504
],
15031505
"icon": "$(terminal)",
@@ -1521,6 +1523,7 @@
15211523
"modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment. ALWAYS call configure_python_environment before using this tool.",
15221524
"toolReferenceName": "pythonInstallPackage",
15231525
"tags": [
1526+
"extension_installed_by_tool",
15241527
"enable_other_tool_configure_python_environment"
15251528
],
15261529
"icon": "$(package)",
@@ -1552,7 +1555,9 @@
15521555
"modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools.",
15531556
"userDescription": "%python.languageModelTools.configure_python_environment.userDescription%",
15541557
"toolReferenceName": "configurePythonEnvironment",
1555-
"tags": [],
1558+
"tags": [
1559+
"extension_installed_by_tool"
1560+
],
15561561
"icon": "$(gear)",
15571562
"canBeReferencedInPrompt": true,
15581563
"inputSchema": {
@@ -1566,6 +1571,46 @@
15661571
"required": []
15671572
},
15681573
"when": "!pythonEnvExtensionInstalled"
1574+
},
1575+
{
1576+
"name": "create_virtual_environment",
1577+
"displayName": "Create a Virtual Environment",
1578+
"modelDescription": "This tool will create a Virual Environment",
1579+
"tags": [
1580+
"extension_installed_by_tool"
1581+
],
1582+
"canBeReferencedInPrompt": false,
1583+
"inputSchema": {
1584+
"type": "object",
1585+
"properties": {
1586+
"resourcePath": {
1587+
"type": "string",
1588+
"description": "The path to the Python file or workspace for which a Python Environment needs to be configured."
1589+
}
1590+
},
1591+
"required": []
1592+
},
1593+
"when": "false"
1594+
},
1595+
{
1596+
"name": "selectEnvironment",
1597+
"displayName": "Select a Python Environment",
1598+
"modelDescription": "This tool will prompt the user to select an existing Python Environment",
1599+
"tags": [
1600+
"extension_installed_by_tool"
1601+
],
1602+
"canBeReferencedInPrompt": false,
1603+
"inputSchema": {
1604+
"type": "object",
1605+
"properties": {
1606+
"resourcePath": {
1607+
"type": "string",
1608+
"description": "The path to the Python file or workspace for which a Python Environment needs to be configured."
1609+
}
1610+
},
1611+
"required": []
1612+
},
1613+
"when": "false"
15691614
}
15701615
]
15711616
},

src/client/chat/configurePythonEnvTool.ts

Lines changed: 49 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,31 @@
33

44
import {
55
CancellationToken,
6-
l10n,
7-
LanguageModelTextPart,
86
LanguageModelTool,
97
LanguageModelToolInvocationOptions,
108
LanguageModelToolInvocationPrepareOptions,
119
LanguageModelToolResult,
1210
PreparedToolInvocation,
1311
Uri,
1412
workspace,
15-
commands,
16-
QuickPickItem,
13+
lm,
1714
} from 'vscode';
18-
import { PythonExtension, ResolvedEnvironment } from '../api/types';
15+
import { PythonExtension } from '../api/types';
1916
import { IServiceContainer } from '../ioc/types';
2017
import { ICodeExecutionService } from '../terminals/types';
2118
import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution';
22-
import { getEnvironmentDetails, getToolResponseIfNotebook, raceCancellationError } from './utils';
19+
import {
20+
getEnvDetailsForResponse,
21+
getToolResponseIfNotebook,
22+
IResourceReference,
23+
isCancellationError,
24+
raceCancellationError,
25+
} from './utils';
2326
import { resolveFilePath } from './utils';
24-
import { IRecommendedEnvironmentService } from '../interpreter/configuration/types';
2527
import { ITerminalHelper } from '../common/terminal/types';
26-
import { raceTimeout } from '../common/utils/async';
27-
import { Commands, Octicons } from '../common/constants';
28-
import { CreateEnvironmentResult } from '../pythonEnvironments/creation/proposed.createEnvApis';
29-
import { IInterpreterPathService } from '../common/types';
30-
import { DisposableStore } from '../common/utils/resourceLifecycle';
31-
import { Common, InterpreterQuickPickList } from '../common/utils/localize';
32-
import { QuickPickItemKind } from '../../test/mocks/vsc';
33-
import { showQuickPick } from '../common/vscodeApis/windowApis';
34-
import { SelectEnvironmentResult } from '../interpreter/configuration/interpreterSelector/commands/setInterpreter';
35-
36-
export interface IResourceReference {
37-
resourcePath?: string;
38-
}
39-
40-
let _environmentConfigured = false;
28+
import { IRecommendedEnvironmentService } from '../interpreter/configuration/types';
29+
import { CreateVirtualEnvTool } from './createVirtualEnvTool';
30+
import { ISelectPythonEnvToolArguments, SelectPythonEnvTool } from './selectEnvTool';
4131

4232
export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceReference> {
4333
private readonly terminalExecutionService: TerminalCodeExecutionProvider;
@@ -47,6 +37,7 @@ export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceRefere
4737
constructor(
4838
private readonly api: PythonExtension['environments'],
4939
private readonly serviceContainer: IServiceContainer,
40+
private readonly createEnvTool: CreateVirtualEnvTool,
5041
) {
5142
this.terminalExecutionService = this.serviceContainer.get<TerminalCodeExecutionProvider>(
5243
ICodeExecutionService,
@@ -57,12 +48,7 @@ export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceRefere
5748
IRecommendedEnvironmentService,
5849
);
5950
}
60-
/**
61-
* Invokes the tool to get the information about the Python environment.
62-
* @param options - The invocation options containing the file path.
63-
* @param token - The cancellation token.
64-
* @returns The result containing the information about the Python environment or an error message.
65-
*/
51+
6652
async invoke(
6753
options: LanguageModelToolInvocationOptions<IResourceReference>,
6854
token: CancellationToken,
@@ -73,22 +59,14 @@ export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceRefere
7359
return notebookResponse;
7460
}
7561

76-
const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource);
77-
// Already selected workspace env, hence nothing to do.
78-
if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) {
79-
return await getEnvDetailsForResponse(
80-
recommededEnv.environment,
81-
this.api,
82-
this.terminalExecutionService,
83-
this.terminalHelper,
84-
resource,
85-
token,
86-
);
87-
}
88-
// No workspace folders, and the user selected a global environment.
89-
if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) {
90-
return await getEnvDetailsForResponse(
91-
recommededEnv.environment,
62+
const workspaceSpecificEnv = await raceCancellationError(
63+
this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource),
64+
token,
65+
);
66+
67+
if (workspaceSpecificEnv) {
68+
return getEnvDetailsForResponse(
69+
workspaceSpecificEnv,
9270
this.api,
9371
this.terminalExecutionService,
9472
this.terminalHelper,
@@ -97,174 +75,46 @@ export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceRefere
9775
);
9876
}
9977

100-
if (!workspace.workspaceFolders?.length) {
101-
const selected = await Promise.resolve(commands.executeCommand(Commands.Set_Interpreter));
102-
const env = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource));
103-
if (selected && env) {
104-
return await getEnvDetailsForResponse(
105-
env,
106-
this.api,
107-
this.terminalExecutionService,
108-
this.terminalHelper,
109-
resource,
110-
token,
111-
);
78+
if (await this.createEnvTool.shouldCreateNewVirtualEnv(resource, token)) {
79+
try {
80+
return await lm.invokeTool(CreateVirtualEnvTool.toolName, options, token);
81+
} catch (ex) {
82+
if (isCancellationError(ex)) {
83+
const input: ISelectPythonEnvToolArguments = {
84+
...options.input,
85+
reason: 'cancelled',
86+
};
87+
// If the user cancelled the tool, then we should invoke the select env tool.
88+
return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token);
89+
}
90+
throw ex;
11291
}
113-
return new LanguageModelToolResult([
114-
new LanguageModelTextPart('User did not select a Python environment.'),
115-
]);
116-
}
117-
118-
const selected = await showCreateAndSelectEnvironmentQuickPick(resource, this.serviceContainer);
119-
const env = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource));
120-
if (selected && env) {
121-
return await getEnvDetailsForResponse(
122-
env,
123-
this.api,
124-
this.terminalExecutionService,
125-
this.terminalHelper,
126-
resource,
127-
token,
128-
);
92+
} else {
93+
const input: ISelectPythonEnvToolArguments = {
94+
...options.input,
95+
};
96+
return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token);
12997
}
130-
return new LanguageModelToolResult([
131-
new LanguageModelTextPart('User did not create nor select a Python environment.'),
132-
]);
13398
}
13499

135100
async prepareInvocation?(
136-
options: LanguageModelToolInvocationPrepareOptions<IResourceReference>,
101+
_options: LanguageModelToolInvocationPrepareOptions<IResourceReference>,
137102
_token: CancellationToken,
138103
): Promise<PreparedToolInvocation> {
139-
if (_environmentConfigured) {
140-
return {};
141-
}
142-
const resource = resolveFilePath(options.input.resourcePath);
143-
if (getToolResponseIfNotebook(resource)) {
144-
return {};
145-
}
104+
return {
105+
invocationMessage: 'Configuring a Python Environment',
106+
};
107+
}
108+
109+
async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) {
146110
const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource);
147111
// Already selected workspace env, hence nothing to do.
148112
if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) {
149-
return {};
113+
return recommededEnv.environment;
150114
}
151115
// No workspace folders, and the user selected a global environment.
152116
if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) {
153-
return {};
154-
}
155-
156-
if (!workspace.workspaceFolders?.length) {
157-
return {
158-
confirmationMessages: {
159-
title: l10n.t('Configure a Python Environment?'),
160-
message: l10n.t('You will be prompted to select a Python Environment.'),
161-
},
162-
};
163-
}
164-
return {
165-
confirmationMessages: {
166-
title: l10n.t('Configure a Python Environment?'),
167-
message: l10n.t(
168-
[
169-
'The recommended option is to create a new Python Environment, providing the benefit of isolating packages from other environments. ',
170-
'Optionally you could select an existing Python Environment.',
171-
].join('\n'),
172-
),
173-
},
174-
};
175-
}
176-
}
177-
178-
async function getEnvDetailsForResponse(
179-
environment: ResolvedEnvironment | undefined,
180-
api: PythonExtension['environments'],
181-
terminalExecutionService: TerminalCodeExecutionProvider,
182-
terminalHelper: ITerminalHelper,
183-
resource: Uri | undefined,
184-
token: CancellationToken,
185-
): Promise<LanguageModelToolResult> {
186-
const envPath = api.getActiveEnvironmentPath(resource);
187-
environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token));
188-
if (!environment || !environment.version) {
189-
throw new Error('No environment found for the provided resource path: ' + resource?.fsPath);
190-
}
191-
const message = await getEnvironmentDetails(
192-
resource,
193-
api,
194-
terminalExecutionService,
195-
terminalHelper,
196-
undefined,
197-
token,
198-
);
199-
return new LanguageModelToolResult([
200-
new LanguageModelTextPart(`A Python Environment has been configured. \n` + message),
201-
]);
202-
}
203-
204-
async function showCreateAndSelectEnvironmentQuickPick(
205-
uri: Uri | undefined,
206-
serviceContainer: IServiceContainer,
207-
): Promise<boolean | undefined> {
208-
const createLabel = `${Octicons.Add} ${InterpreterQuickPickList.create.label}`;
209-
const selectLabel = l10n.t('Select an existing Python Environment');
210-
const items: QuickPickItem[] = [
211-
{ kind: QuickPickItemKind.Separator, label: Common.recommended },
212-
{ label: createLabel },
213-
{ label: selectLabel },
214-
];
215-
216-
const selectedItem = await showQuickPick(items, {
217-
placeHolder: l10n.t('Configure a Python Environment'),
218-
matchOnDescription: true,
219-
ignoreFocusOut: true,
220-
});
221-
222-
if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === createLabel) {
223-
const disposables = new DisposableStore();
224-
try {
225-
const workspaceFolder =
226-
(workspace.workspaceFolders?.length && uri ? workspace.getWorkspaceFolder(uri) : undefined) ||
227-
(workspace.workspaceFolders?.length === 1 ? workspace.workspaceFolders[0] : undefined);
228-
const interpreterPathService = serviceContainer.get<IInterpreterPathService>(IInterpreterPathService);
229-
const interpreterChanged = new Promise<void>((resolve) => {
230-
disposables.add(interpreterPathService.onDidChange(() => resolve()));
231-
});
232-
const created: CreateEnvironmentResult | undefined = await commands.executeCommand(
233-
Commands.Create_Environment,
234-
{
235-
showBackButton: true,
236-
selectEnvironment: true,
237-
workspaceFolder,
238-
},
239-
);
240-
241-
if (created?.action === 'Back') {
242-
return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer);
243-
}
244-
if (created?.action === 'Cancel') {
245-
return undefined;
246-
}
247-
if (created?.path) {
248-
// Wait a few secs to ensure the env is selected as the active environment..
249-
await raceTimeout(5_000, interpreterChanged);
250-
return true;
251-
}
252-
} finally {
253-
disposables.dispose();
254-
}
255-
}
256-
if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === selectLabel) {
257-
const result = (await Promise.resolve(
258-
commands.executeCommand(Commands.Set_Interpreter, { hideCreateVenv: true, showBackButton: true }),
259-
)) as SelectEnvironmentResult | undefined;
260-
if (result?.action === 'Back') {
261-
return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer);
262-
}
263-
if (result?.action === 'Cancel') {
264-
return undefined;
265-
}
266-
if (result?.path) {
267-
return true;
117+
return recommededEnv.environment;
268118
}
269119
}
270120
}

0 commit comments

Comments
 (0)