Skip to content

Add get package copilot tool #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 27, 2025
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 33 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,31 @@
{
"type": "python"
}
],
"languageModelTools": [
{
"name": "python_get_packages",
"displayName": "Get Python Packages",
"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.",
"toolReferenceName": "pythonGetPackages",
"tags": [
"vscode_editing"
],
"icon": "$(files)",
"inputSchema": {
"type": "object",
"properties": {
"filePath": {
"type": "string"
}
},
"description": "The path to the Python file or workspace to get the installed packages for.",
"required": [
"filePath"
]
},
"canBeReferencedInPrompt": true
}
]
},
"scripts": {
Expand All @@ -465,7 +490,7 @@
"@types/node": "20.2.5",
"@types/sinon": "^17.0.3",
"@types/stack-trace": "0.0.29",
"@types/vscode": "^1.93.0",
"@types/vscode": "^1.96.0",
"@types/which": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^8.16.0",
"@typescript-eslint/parser": "^8.16.0",
Expand All @@ -491,5 +516,10 @@
"stack-trace": "0.0.10",
"vscode-jsonrpc": "^9.0.0-next.5",
"which": "^4.0.0"
}
}
},
"enabledApiProposals": [
"chatParticipantPrivate",
"chatParticipantAdditions",
"chatVariableResolver"
]
}
4 changes: 4 additions & 0 deletions src/common/lm.apis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as vscode from 'vscode';
export function registerTools<T>(name: string, tool: vscode.LanguageModelTool<T>): vscode.Disposable {
return vscode.lm.registerTool(name, tool);
}
92 changes: 92 additions & 0 deletions src/copilotTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
CancellationToken,
LanguageModelTextPart,
LanguageModelTool,
LanguageModelToolInvocationOptions,
LanguageModelToolInvocationPrepareOptions,
LanguageModelToolResult,
PreparedToolInvocation,
Uri,
} from 'vscode';
import { PythonPackageGetterApi, PythonProjectEnvironmentApi } from './api';
import { createDeferred } from './common/utils/deferred';

export interface IGetActiveFile {
filePath?: string;
}

/**
* A tool to get the list of installed Python packages in the active environment.
*/
export class GetPackagesTool implements LanguageModelTool<IGetActiveFile> {
constructor(private readonly api: PythonProjectEnvironmentApi & PythonPackageGetterApi) {}
/**
* Invokes the tool to get the list of installed packages.
* @param options - The invocation options containing the file path.
* @param token - The cancellation token.
* @returns The result containing the list of installed packages or an error message.
*/
async invoke(
options: LanguageModelToolInvocationOptions<IGetActiveFile>,
token: CancellationToken,
): Promise<LanguageModelToolResult> {
const deferredReturn = createDeferred<LanguageModelToolResult>();
token.onCancellationRequested(() => {
const errorMessage: string = `Operation cancelled by the user.`;
deferredReturn.resolve({ content: [new LanguageModelTextPart(errorMessage)] } as LanguageModelToolResult);
});

const parameters: IGetActiveFile = options.input;

if (parameters.filePath === undefined || parameters.filePath === '') {
throw new Error('Invalid input: filePath is required');
}
const fileUri = Uri.file(parameters.filePath);

try {
const environment = await this.api.getEnvironment(fileUri);
if (!environment) {
// Check if the file is a notebook or a notebook cell to throw specific error messages.
if (fileUri.fsPath.endsWith('.ipynb') || fileUri.fsPath.includes('.ipynb#')) {
throw new Error('Unable to access Jupyter kernels for notebook cells');
}
throw new Error('No environment found');
}
await this.api.refreshPackages(environment);
const installedPackages = await this.api.getPackages(environment);

let resultMessage: string;
if (!installedPackages || installedPackages.length === 0) {
resultMessage = 'No packages are installed in the current environment.';
} else {
const packageNames = installedPackages
.map((pkg) => pkg.version ? `${pkg.name} (${pkg.version})` : pkg.name)
.join(', ');
resultMessage = 'The packages installed in the current environment are as follows:\n' + packageNames;
}

const textPart = new LanguageModelTextPart(resultMessage || '');
deferredReturn.resolve({ content: [textPart] });
} catch (error) {
const errorMessage: string = `An error occurred while fetching packages: ${error}`;
deferredReturn.resolve({ content: [new LanguageModelTextPart(errorMessage)] } as LanguageModelToolResult);
}
return deferredReturn.promise;
}

/**
* Prepares the invocation of the tool.
* @param _options - The preparation options.
* @param _token - The cancellation token.
* @returns The prepared tool invocation.
*/
async prepareInvocation?(
_options: LanguageModelToolInvocationPrepareOptions<IGetActiveFile>,
_token: CancellationToken,
): Promise<PreparedToolInvocation> {
const message = 'Preparing to fetch the list of installed Python packages...';
return {
invocationMessage: message,
};
}
}
6 changes: 6 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import { EventNames } from './common/telemetry/constants';
import { ensureCorrectVersion } from './common/extVersion';
import { ExistingProjects } from './features/creators/existingProjects';
import { AutoFindProjects } from './features/creators/autoFindProjects';
import { GetPackagesTool } from './copilotTools';
import { registerTools } from './common/lm.apis';

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

context.subscriptions.push(
registerCompletionProvider(envManagers),

registerTools('python_get_packages', new GetPackagesTool(api)),
commands.registerCommand('python-envs.viewLogs', () => outputChannel.show()),
commands.registerCommand('python-envs.refreshManager', async (item) => {
await refreshManagerCommand(item);
Expand Down Expand Up @@ -237,6 +241,8 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
});

sendTelemetryEvent(EventNames.EXTENSION_ACTIVATION_DURATION, start.elapsedTime);


return api;
}

Expand Down
Loading
Loading