Skip to content

Commit 7f6410a

Browse files
authored
Add get package copilot tool (#135)
add new copilot tool `pythonGetPackages` that can be called by the user or other agents / tools
1 parent 00f1d2f commit 7f6410a

13 files changed

+1068
-12
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,31 @@
444444
{
445445
"type": "python"
446446
}
447+
],
448+
"languageModelTools": [
449+
{
450+
"name": "python_get_packages",
451+
"displayName": "Get Python Packages",
452+
"modelDescription": "Returns the packages installed in the given Python file's environment. You should call this when you want to generate Python code to determine the users preferred packages. Also call this to determine if you need to provide installation instructions in a response.",
453+
"toolReferenceName": "pythonGetPackages",
454+
"tags": [
455+
"vscode_editing"
456+
],
457+
"icon": "$(files)",
458+
"inputSchema": {
459+
"type": "object",
460+
"properties": {
461+
"filePath": {
462+
"type": "string"
463+
}
464+
},
465+
"description": "The path to the Python file or workspace to get the installed packages for.",
466+
"required": [
467+
"filePath"
468+
]
469+
},
470+
"canBeReferencedInPrompt": true
471+
}
447472
]
448473
},
449474
"scripts": {
@@ -465,7 +490,7 @@
465490
"@types/node": "20.2.5",
466491
"@types/sinon": "^17.0.3",
467492
"@types/stack-trace": "0.0.29",
468-
"@types/vscode": "^1.93.0",
493+
"@types/vscode": "^1.96.0",
469494
"@types/which": "^3.0.4",
470495
"@typescript-eslint/eslint-plugin": "^8.16.0",
471496
"@typescript-eslint/parser": "^8.16.0",
@@ -491,5 +516,10 @@
491516
"stack-trace": "0.0.10",
492517
"vscode-jsonrpc": "^9.0.0-next.5",
493518
"which": "^4.0.0"
494-
}
495-
}
519+
},
520+
"enabledApiProposals": [
521+
"chatParticipantPrivate",
522+
"chatParticipantAdditions",
523+
"chatVariableResolver"
524+
]
525+
}

src/common/lm.apis.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import * as vscode from 'vscode';
2+
export function registerTools<T>(name: string, tool: vscode.LanguageModelTool<T>): vscode.Disposable {
3+
return vscode.lm.registerTool(name, tool);
4+
}

src/copilotTools.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
CancellationToken,
3+
LanguageModelTextPart,
4+
LanguageModelTool,
5+
LanguageModelToolInvocationOptions,
6+
LanguageModelToolInvocationPrepareOptions,
7+
LanguageModelToolResult,
8+
PreparedToolInvocation,
9+
Uri,
10+
} from 'vscode';
11+
import { PythonPackageGetterApi, PythonProjectEnvironmentApi } from './api';
12+
import { createDeferred } from './common/utils/deferred';
13+
14+
export interface IGetActiveFile {
15+
filePath?: string;
16+
}
17+
18+
/**
19+
* A tool to get the list of installed Python packages in the active environment.
20+
*/
21+
export class GetPackagesTool implements LanguageModelTool<IGetActiveFile> {
22+
constructor(private readonly api: PythonProjectEnvironmentApi & PythonPackageGetterApi) {}
23+
/**
24+
* Invokes the tool to get the list of installed packages.
25+
* @param options - The invocation options containing the file path.
26+
* @param token - The cancellation token.
27+
* @returns The result containing the list of installed packages or an error message.
28+
*/
29+
async invoke(
30+
options: LanguageModelToolInvocationOptions<IGetActiveFile>,
31+
token: CancellationToken,
32+
): Promise<LanguageModelToolResult> {
33+
const deferredReturn = createDeferred<LanguageModelToolResult>();
34+
token.onCancellationRequested(() => {
35+
const errorMessage: string = `Operation cancelled by the user.`;
36+
deferredReturn.resolve({ content: [new LanguageModelTextPart(errorMessage)] } as LanguageModelToolResult);
37+
});
38+
39+
const parameters: IGetActiveFile = options.input;
40+
41+
if (parameters.filePath === undefined || parameters.filePath === '') {
42+
throw new Error('Invalid input: filePath is required');
43+
}
44+
const fileUri = Uri.file(parameters.filePath);
45+
46+
try {
47+
const environment = await this.api.getEnvironment(fileUri);
48+
if (!environment) {
49+
// Check if the file is a notebook or a notebook cell to throw specific error messages.
50+
if (fileUri.fsPath.endsWith('.ipynb') || fileUri.fsPath.includes('.ipynb#')) {
51+
throw new Error('Unable to access Jupyter kernels for notebook cells');
52+
}
53+
throw new Error('No environment found');
54+
}
55+
await this.api.refreshPackages(environment);
56+
const installedPackages = await this.api.getPackages(environment);
57+
58+
let resultMessage: string;
59+
if (!installedPackages || installedPackages.length === 0) {
60+
resultMessage = 'No packages are installed in the current environment.';
61+
} else {
62+
const packageNames = installedPackages
63+
.map((pkg) => pkg.version ? `${pkg.name} (${pkg.version})` : pkg.name)
64+
.join(', ');
65+
resultMessage = 'The packages installed in the current environment are as follows:\n' + packageNames;
66+
}
67+
68+
const textPart = new LanguageModelTextPart(resultMessage || '');
69+
deferredReturn.resolve({ content: [textPart] });
70+
} catch (error) {
71+
const errorMessage: string = `An error occurred while fetching packages: ${error}`;
72+
deferredReturn.resolve({ content: [new LanguageModelTextPart(errorMessage)] } as LanguageModelToolResult);
73+
}
74+
return deferredReturn.promise;
75+
}
76+
77+
/**
78+
* Prepares the invocation of the tool.
79+
* @param _options - The preparation options.
80+
* @param _token - The cancellation token.
81+
* @returns The prepared tool invocation.
82+
*/
83+
async prepareInvocation?(
84+
_options: LanguageModelToolInvocationPrepareOptions<IGetActiveFile>,
85+
_token: CancellationToken,
86+
): Promise<PreparedToolInvocation> {
87+
const message = 'Preparing to fetch the list of installed Python packages...';
88+
return {
89+
invocationMessage: message,
90+
};
91+
}
92+
}

src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import { EventNames } from './common/telemetry/constants';
5555
import { ensureCorrectVersion } from './common/extVersion';
5656
import { ExistingProjects } from './features/creators/existingProjects';
5757
import { AutoFindProjects } from './features/creators/autoFindProjects';
58+
import { GetPackagesTool } from './copilotTools';
59+
import { registerTools } from './common/lm.apis';
5860

5961
export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi> {
6062
const start = new StopWatch();
@@ -103,6 +105,8 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
103105

104106
context.subscriptions.push(
105107
registerCompletionProvider(envManagers),
108+
109+
registerTools('python_get_packages', new GetPackagesTool(api)),
106110
commands.registerCommand('python-envs.viewLogs', () => outputChannel.show()),
107111
commands.registerCommand('python-envs.refreshManager', async (item) => {
108112
await refreshManagerCommand(item);
@@ -238,6 +242,8 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
238242
});
239243

240244
sendTelemetryEvent(EventNames.EXTENSION_ACTIVATION_DURATION, start.elapsedTime);
245+
246+
241247
return api;
242248
}
243249

0 commit comments

Comments
 (0)