Skip to content

Commit 49ad7af

Browse files
committed
Configuration tool with improved workflow and UX
1 parent 2db24aa commit 49ad7af

16 files changed

+681
-304
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 & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,40 @@
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 { IDiscoveryAPI } from '../pythonEnvironments/base/locator';
30+
import { CreateVirtualEnvTool } from './createVirtualEnvTool';
31+
import { SelectPythonEnvTool } from './selectEnvTool';
4132

4233
export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceReference> {
4334
private readonly terminalExecutionService: TerminalCodeExecutionProvider;
4435
private readonly terminalHelper: ITerminalHelper;
4536
private readonly recommendedEnvService: IRecommendedEnvironmentService;
4637
public static readonly toolName = 'configure_python_environment';
4738
constructor(
39+
private readonly discoveryApi: IDiscoveryAPI,
4840
private readonly api: PythonExtension['environments'],
4941
private readonly serviceContainer: IServiceContainer,
5042
) {
@@ -57,12 +49,7 @@ export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceRefere
5749
IRecommendedEnvironmentService,
5850
);
5951
}
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-
*/
52+
6653
async invoke(
6754
options: LanguageModelToolInvocationOptions<IResourceReference>,
6855
token: CancellationToken,
@@ -73,22 +60,14 @@ export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceRefere
7360
return notebookResponse;
7461
}
7562

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,
63+
const workspaceSpecificEnv = await raceCancellationError(
64+
this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource),
65+
token,
66+
);
67+
68+
if (workspaceSpecificEnv) {
69+
return getEnvDetailsForResponse(
70+
workspaceSpecificEnv,
9271
this.api,
9372
this.terminalExecutionService,
9473
this.terminalHelper,
@@ -97,174 +76,46 @@ export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceRefere
9776
);
9877
}
9978

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-
);
79+
let reason: 'cancelled' | undefined;
80+
if (
81+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
82+
await new CreateVirtualEnvTool(this.discoveryApi, this.api, this.serviceContainer).canCreateNewVirtualEnv(
83+
resolveFilePath(options.input.resourcePath),
84+
token,
85+
)
86+
) {
87+
reason = 'cancelled';
88+
try {
89+
return await lm.invokeTool(CreateVirtualEnvTool.toolName, options, token);
90+
} catch (ex) {
91+
// If the user cancelled the tool, then we should not invoke the select env tool.
92+
if (!isCancellationError(ex)) {
93+
throw ex;
94+
}
11295
}
113-
return new LanguageModelToolResult([
114-
new LanguageModelTextPart('User did not select a Python environment.'),
115-
]);
11696
}
11797

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-
);
129-
}
130-
return new LanguageModelToolResult([
131-
new LanguageModelTextPart('User did not create nor select a Python environment.'),
132-
]);
98+
return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input: { ...options.input, reason } }, token);
13399
}
134100

135101
async prepareInvocation?(
136-
options: LanguageModelToolInvocationPrepareOptions<IResourceReference>,
102+
_options: LanguageModelToolInvocationPrepareOptions<IResourceReference>,
137103
_token: CancellationToken,
138104
): Promise<PreparedToolInvocation> {
139-
if (_environmentConfigured) {
140-
return {};
141-
}
142-
const resource = resolveFilePath(options.input.resourcePath);
143-
if (getToolResponseIfNotebook(resource)) {
144-
return {};
145-
}
105+
return {
106+
invocationMessage: 'Configuring a Python Environment',
107+
};
108+
}
109+
110+
async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) {
146111
const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource);
147112
// Already selected workspace env, hence nothing to do.
148113
if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) {
149-
return {};
114+
return recommededEnv.environment;
150115
}
151116
// No workspace folders, and the user selected a global environment.
152117
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;
118+
return recommededEnv.environment;
268119
}
269120
}
270121
}

0 commit comments

Comments
 (0)