Skip to content

Commit 9bdf9db

Browse files
committed
wip
1 parent a653a6e commit 9bdf9db

8 files changed

Lines changed: 285 additions & 4 deletions

File tree

package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,27 @@
14861486
"required": []
14871487
}
14881488
},
1489+
{
1490+
"name": "create_python_virtual_environment",
1491+
"displayName": "Create Python Virtual Environment",
1492+
"modelDescription": "This tool will create a Python virtual environment in the specified workspace.",
1493+
"toolReferenceName": "createPythonVirtualEnvironment",
1494+
"tags": [
1495+
"ms-python.python"
1496+
],
1497+
"icon": "$(files)",
1498+
"canBeReferencedInPrompt": false,
1499+
"inputSchema": {
1500+
"type": "object",
1501+
"properties": {
1502+
"resourcePath": {
1503+
"type": "string"
1504+
}
1505+
},
1506+
"description": "The path to the Python file or workspace to get the environment information for.",
1507+
"required": []
1508+
}
1509+
},
14891510
{
14901511
"name": "get_python_executable",
14911512
"displayName": "Get Python Executable",
@@ -1562,6 +1583,29 @@
15621583
]
15631584
},
15641585
"when": "!pythonEnvExtensionInstalled"
1586+
},
1587+
{
1588+
"name": "configure_python_environment",
1589+
"displayName": "Configure Python Environment",
1590+
"userDescription": "%python.languageModelTools.configure_python_environment.userDescription%",
1591+
"modelDescription": "Configures the Python environment in the given workspace. Use this tool to set up the user's chosen environment.",
1592+
"toolReferenceName": "pythonConfigureEnvironment",
1593+
"tags": [
1594+
"ms-python.python"
1595+
],
1596+
"icon": "$(package)",
1597+
"canBeReferencedInPrompt": true,
1598+
"inputSchema": {
1599+
"type": "object",
1600+
"properties": {
1601+
"resourcePath": {
1602+
"type": "string",
1603+
"description": "The path to the Python file or workspace for which the Python environment is to be configured."
1604+
}
1605+
},
1606+
"required": []
1607+
},
1608+
"when": "!pythonEnvExtensionInstalled"
15651609
}
15661610
]
15671611
},
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
lm,
6+
CancellationError,
7+
CancellationToken,
8+
l10n,
9+
LanguageModelTextPart,
10+
LanguageModelTool,
11+
LanguageModelToolInvocationOptions,
12+
LanguageModelToolInvocationPrepareOptions,
13+
LanguageModelToolResult,
14+
PreparedToolInvocation,
15+
} from 'vscode';
16+
import { PythonExtension } from '../api/types';
17+
import { IServiceContainer } from '../ioc/types';
18+
import { ICodeExecutionService } from '../terminals/types';
19+
import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution';
20+
import { getEnvironmentDetails, raceCancellationError } from './utils';
21+
import { resolveFilePath } from './utils';
22+
import { IInterpreterQuickPick } from '../interpreter/configuration/types';
23+
import { ITerminalHelper } from '../common/terminal/types';
24+
import { StopWatch } from '../common/utils/stopWatch';
25+
import { sleep } from '../common/utils/async';
26+
import { CreateVenvTool } from './createVenvTool';
27+
28+
export interface IResourceReference {
29+
resourcePath?: string;
30+
}
31+
32+
export class ConfigurePythonEnvTool implements LanguageModelTool<IResourceReference> {
33+
public static get EnvironmentConfigured() {
34+
return ConfigurePythonEnvTool._environmentConfigured;
35+
}
36+
private static _environmentConfigured = false;
37+
private readonly terminalExecutionService: TerminalCodeExecutionProvider;
38+
private readonly interpreterPicker: IInterpreterQuickPick;
39+
private readonly terminalHelper: ITerminalHelper;
40+
public static readonly toolName = 'configure_python_environment';
41+
constructor(
42+
private readonly api: PythonExtension['environments'],
43+
private readonly serviceContainer: IServiceContainer,
44+
) {
45+
this.terminalExecutionService = this.serviceContainer.get<TerminalCodeExecutionProvider>(
46+
ICodeExecutionService,
47+
'standard',
48+
);
49+
this.interpreterPicker = this.serviceContainer.get<IInterpreterQuickPick>(IInterpreterQuickPick);
50+
this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper);
51+
}
52+
/**
53+
* Invokes the tool to get the information about the Python environment.
54+
* @param options - The invocation options containing the file path.
55+
* @param token - The cancellation token.
56+
* @returns The result containing the information about the Python environment or an error message.
57+
*/
58+
async invoke(
59+
options: LanguageModelToolInvocationOptions<IResourceReference>,
60+
token: CancellationToken,
61+
): Promise<LanguageModelToolResult> {
62+
const resourcePath = resolveFilePath(options.input.resourcePath);
63+
64+
try {
65+
if (!ConfigurePythonEnvTool.EnvironmentConfigured) {
66+
// Try to create one.
67+
const result = await lm.invokeTool(CreateVenvTool.toolName, { resourcePath: options.input.resourcePath } as any, token);
68+
console.log('CreateVenvTool result:', result);
69+
const interpreterPath = await this.interpreterPicker.getInterpreterViaQuickPick(
70+
resourcePath,
71+
undefined,
72+
{
73+
showCreateEnvironment: true,
74+
},
75+
);
76+
if (!interpreterPath) {
77+
return new LanguageModelToolResult([
78+
new LanguageModelTextPart('No Python Environment configured.'),
79+
]);
80+
}
81+
ConfigurePythonEnvTool._environmentConfigured = true;
82+
83+
const stopWatch = new StopWatch();
84+
while (stopWatch.elapsedTime < 5_000) {
85+
try {
86+
await this.api.getActiveEnvironmentPath(resourcePath);
87+
} catch {
88+
await sleep(500);
89+
continue;
90+
}
91+
}
92+
}
93+
// environment
94+
const envPath = this.api.getActiveEnvironmentPath(resourcePath);
95+
const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token);
96+
if (!environment || !environment.version) {
97+
throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath);
98+
}
99+
const message = await getEnvironmentDetails(
100+
resourcePath,
101+
this.api,
102+
this.terminalExecutionService,
103+
this.terminalHelper,
104+
undefined,
105+
token,
106+
);
107+
return new LanguageModelToolResult([new LanguageModelTextPart(message)]);
108+
} catch (error) {
109+
if (error instanceof CancellationError) {
110+
throw error;
111+
}
112+
const errorMessage: string = `An error occurred while fetching environment information: ${error}`;
113+
return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]);
114+
}
115+
}
116+
117+
async prepareInvocation?(
118+
_options: LanguageModelToolInvocationPrepareOptions<IResourceReference>,
119+
_token: CancellationToken,
120+
): Promise<PreparedToolInvocation> {
121+
if (ConfigurePythonEnvTool._environmentConfigured) {
122+
return {};
123+
}
124+
return {
125+
confirmationMessages: {
126+
title: l10n.t('Configure your Python Environment?'),
127+
message: l10n.t(
128+
'You can either select a Python environment or create a new Environment. \nThe latter being the recommended option.',
129+
),
130+
},
131+
invocationMessage: l10n.t('Configuring Python environment'),
132+
};
133+
}
134+
}

src/client/chat/createVenvTool.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
CancellationToken,
6+
l10n,
7+
LanguageModelTextPart,
8+
LanguageModelTool,
9+
LanguageModelToolInvocationOptions,
10+
LanguageModelToolInvocationPrepareOptions,
11+
LanguageModelToolResult,
12+
PreparedToolInvocation,
13+
} from 'vscode';
14+
15+
export interface IResourceReference {
16+
resourcePath?: string;
17+
}
18+
19+
export class CreateVenvTool implements LanguageModelTool<IResourceReference> {
20+
public static get EnvironmnentCreated() {
21+
return CreateVenvTool._envCreated;
22+
}
23+
private static _envCreated = false;
24+
public static readonly toolName = 'create_python_virtual_environment';
25+
constructor() // private readonly api: PythonExtension['environments'],
26+
// private readonly serviceContainer: IServiceContainer,
27+
{
28+
// this.terminalExecutionService = this.serviceContainer.get<TerminalCodeExecutionProvider>(
29+
// ICodeExecutionService,
30+
// 'standard',
31+
// );
32+
// this.interpreterPicker = this.serviceContainer.get<IInterpreterQuickPick>(IInterpreterQuickPick);
33+
// this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper);
34+
}
35+
/**
36+
* Invokes the tool to get the information about the Python environment.
37+
* @param options - The invocation options containing the file path.
38+
* @param token - The cancellation token.
39+
* @returns The result containing the information about the Python environment or an error message.
40+
*/
41+
async invoke(
42+
_options: LanguageModelToolInvocationOptions<IResourceReference>,
43+
_token: CancellationToken,
44+
): Promise<LanguageModelToolResult> {
45+
const errorMessage: string = `Successfully created a Python environment.`;
46+
return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]);
47+
}
48+
49+
async prepareInvocation?(
50+
_options: LanguageModelToolInvocationPrepareOptions<IResourceReference>,
51+
_token: CancellationToken,
52+
): Promise<PreparedToolInvocation> {
53+
if (CreateVenvTool._envCreated) {
54+
return {};
55+
}
56+
return {
57+
confirmationMessages: {
58+
title: l10n.t('Create Virtual Python Environment?'),
59+
message: l10n.t('A Python virtual environment will be created in the current workspace.'),
60+
},
61+
invocationMessage: l10n.t('Creating Python virtual environment'),
62+
};
63+
}
64+
}

src/client/chat/getExecutableTool.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { resolveFilePath } from './utils';
2121
import { traceError } from '../logging';
2222
import { ITerminalHelper } from '../common/terminal/types';
2323
import { IDiscoveryAPI } from '../pythonEnvironments/base/locator';
24+
import { ConfigurePythonEnvTool } from './configurePythonEnvTool';
2425

2526
export interface IResourceReference {
2627
resourcePath?: string;
@@ -45,6 +46,18 @@ export class GetExecutableTool implements LanguageModelTool<IResourceReference>
4546
options: LanguageModelToolInvocationOptions<IResourceReference>,
4647
token: CancellationToken,
4748
): Promise<LanguageModelToolResult> {
49+
if (!ConfigurePythonEnvTool.EnvironmentConfigured) {
50+
return new LanguageModelToolResult([
51+
new LanguageModelTextPart(
52+
[
53+
`A Python environment is not configured. Please configure a Python environment first using the ${ConfigurePythonEnvTool.toolName}.`,
54+
`The ${ConfigurePythonEnvTool.toolName} tool will guide the user through the process of configuring a Python environment.`,
55+
'Once the environment is configured, you can use this tool to get the Python executable information.',
56+
].join('\n'),
57+
),
58+
]);
59+
}
60+
4861
const resourcePath = resolveFilePath(options.input.resourcePath);
4962

5063
try {

src/client/chat/getPythonEnvTool.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { getEnvironmentDetails, raceCancellationError } from './utils';
2121
import { resolveFilePath } from './utils';
2222
import { getPythonPackagesResponse } from './listPackagesTool';
2323
import { ITerminalHelper } from '../common/terminal/types';
24+
import { ConfigurePythonEnvTool } from './configurePythonEnvTool';
2425

2526
export interface IResourceReference {
2627
resourcePath?: string;
@@ -54,6 +55,18 @@ export class GetEnvironmentInfoTool implements LanguageModelTool<IResourceRefere
5455
options: LanguageModelToolInvocationOptions<IResourceReference>,
5556
token: CancellationToken,
5657
): Promise<LanguageModelToolResult> {
58+
if (!ConfigurePythonEnvTool.EnvironmentConfigured) {
59+
return new LanguageModelToolResult([
60+
new LanguageModelTextPart(
61+
[
62+
`A Python environment is not configured. Please configure a Python environment first using the ${ConfigurePythonEnvTool.toolName}.`,
63+
`The ${ConfigurePythonEnvTool.toolName} tool will guide the user through the process of configuring a Python environment.`,
64+
'Once the environment is configured, you can use this tool to get the Python executable information.',
65+
].join('\n'),
66+
),
67+
]);
68+
}
69+
5770
const resourcePath = resolveFilePath(options.input.resourcePath);
5871

5972
try {

src/client/chat/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { IDiscoveryAPI } from '../pythonEnvironments/base/locator';
1212
import { ListPythonPackagesTool } from './listPackagesTool';
1313
import { GetExecutableTool } from './getExecutableTool';
1414
import { GetEnvironmentInfoTool } from './getPythonEnvTool';
15+
import { ConfigurePythonEnvTool } from './configurePythonEnvTool';
16+
import { CreateVenvTool } from './createVenvTool';
1517

1618
export function registerTools(
1719
context: IExtensionContext,
@@ -48,6 +50,10 @@ export function registerTools(
4850
new InstallPackagesTool(environmentsApi, serviceContainer, discoverApi),
4951
),
5052
);
53+
ourTools.add(
54+
lm.registerTool(ConfigurePythonEnvTool.toolName, new ConfigurePythonEnvTool(environmentsApi, serviceContainer)),
55+
);
56+
ourTools.add(lm.registerTool(CreateVenvTool.toolName, new CreateVenvTool()));
5157
ourTools.add(
5258
extensions.onDidChange(() => {
5359
const envExtension = extensions.getExtension(ENVS_EXTENSION_ID);

src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { BaseInterpreterSelectorCommand } from './base';
4949
import { untildify } from '../../../../common/helpers';
5050
import { useEnvExtension } from '../../../../envExt/api.internal';
5151
import { setInterpreterLegacy } from '../../../../envExt/api.legacy';
52+
import { CreateEnvironmentResult } from '../../../../pythonEnvironments/creation/proposed.createEnvApis';
5253

5354
export type InterpreterStateArgs = { path?: string; workspace: Resource };
5455
export type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem;
@@ -229,12 +230,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem
229230
sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_OR_FIND);
230231
return this._enterOrBrowseInterpreterPath.bind(this);
231232
} else if (selection.label === this.createEnvironmentSuggestion.label) {
232-
this.commandManager
233-
.executeCommand(Commands.Create_Environment, {
233+
const createdEnv = (await Promise.resolve(
234+
this.commandManager.executeCommand(Commands.Create_Environment, {
234235
showBackButton: false,
235236
selectEnvironment: true,
236-
})
237-
.then(noop, noop);
237+
}),
238+
).catch(noop)) as CreateEnvironmentResult | undefined;
239+
state.path = createdEnv?.path;
238240
} else if (selection.label === this.noPythonInstalled.label) {
239241
this.commandManager.executeCommand(Commands.InstallPython).then(noop, noop);
240242
this.wasNoPythonInstalledItemClicked = true;

src/client/jupyter/jupyterIntegration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ type PythonApiForJupyterExtension = {
6363
* @param func : The function that Python should call when requesting the Python path.
6464
*/
6565
registerJupyterPythonPathFunction(func: (uri: Uri) => Promise<string | undefined>): void;
66+
67+
/**
68+
* Returns the Environment that was last used in a Python tool.
69+
*/
70+
getLastUsedEnvInTool(uri: Uri): Promise<Environment | undefined>;
6671
};
6772

6873
type JupyterExtensionApi = {

0 commit comments

Comments
 (0)