From 0890a10e11949f58897428a1f9c26831d599595c Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:36:52 -0700 Subject: [PATCH 1/3] first pass --- package-lock.json | 30 +++ package.json | 13 +- package.nls.json | 3 +- src/extension.ts | 2 + src/features/chat/chatParticipant.tsx | 66 ++++++ src/features/chat/prompts.tsx | 94 +++++++++ .../chat/pythonHelperChatParticipant.ts | 29 +++ src/features/chat/send_prompt.ts | 198 ++++++++++++++++++ src/managers/builtin/pipManager.ts | 58 ++++- tsconfig.json | 3 + webpack.config.js | 4 +- 11 files changed, 490 insertions(+), 10 deletions(-) create mode 100644 src/features/chat/chatParticipant.tsx create mode 100644 src/features/chat/prompts.tsx create mode 100644 src/features/chat/pythonHelperChatParticipant.ts create mode 100644 src/features/chat/send_prompt.ts diff --git a/package-lock.json b/package-lock.json index aa0b2fc7..af3ffc9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.3.0-dev", "dependencies": { "@iarna/toml": "^2.2.5", + "@vscode/chat-extension-utils": "^0.0.0-alpha.1", "@vscode/extension-telemetry": "^0.9.7", + "@vscode/prompt-tsx": "^0.3.0-alpha.12", "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", "fs-extra": "^11.2.0", @@ -964,6 +966,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vscode/chat-extension-utils": { + "version": "0.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@vscode/chat-extension-utils/-/chat-extension-utils-0.0.0-alpha.5.tgz", + "integrity": "sha512-EkfetTIGMDyClZkIx8oMOhprlXufnj0b/G1W4QGg4jhkWVUBE7kLRZsqEnpNjmtxHTugzc61gPQwT3zgH3HXgA==", + "license": "MIT", + "dependencies": { + "@vscode/prompt-tsx": "^0.3.0-alpha.14" + } + }, "node_modules/@vscode/extension-telemetry": { "version": "0.9.7", "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.7.tgz", @@ -978,6 +989,12 @@ "vscode": "^1.75.0" } }, + "node_modules/@vscode/prompt-tsx": { + "version": "0.3.0-alpha.24", + "resolved": "https://registry.npmjs.org/@vscode/prompt-tsx/-/prompt-tsx-0.3.0-alpha.24.tgz", + "integrity": "sha512-WUz6rPLcN6F64WxxwTiLzHOuhUcdLKBWMckppn43DBC1Ba67Lvd9RV+2LOxj938YzvEVOKGoAY/qgRtXd77I7Q==", + "license": "MIT" + }, "node_modules/@vscode/test-cli": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", @@ -6135,6 +6152,14 @@ } } }, + "@vscode/chat-extension-utils": { + "version": "0.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@vscode/chat-extension-utils/-/chat-extension-utils-0.0.0-alpha.5.tgz", + "integrity": "sha512-EkfetTIGMDyClZkIx8oMOhprlXufnj0b/G1W4QGg4jhkWVUBE7kLRZsqEnpNjmtxHTugzc61gPQwT3zgH3HXgA==", + "requires": { + "@vscode/prompt-tsx": "^0.3.0-alpha.14" + } + }, "@vscode/extension-telemetry": { "version": "0.9.7", "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.7.tgz", @@ -6145,6 +6170,11 @@ "@microsoft/applicationinsights-web-basic": "^3.3.0" } }, + "@vscode/prompt-tsx": { + "version": "0.3.0-alpha.24", + "resolved": "https://registry.npmjs.org/@vscode/prompt-tsx/-/prompt-tsx-0.3.0-alpha.24.tgz", + "integrity": "sha512-WUz6rPLcN6F64WxxwTiLzHOuhUcdLKBWMckppn43DBC1Ba67Lvd9RV+2LOxj938YzvEVOKGoAY/qgRtXd77I7Q==" + }, "@vscode/test-cli": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", diff --git a/package.json b/package.json index 7886ed16..0e81bcd2 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,15 @@ "main": "./dist/extension.js", "icon": "icon.png", "contributes": { + "chatParticipants": [ + { + "name": "pythonHelper", + "id": "python-helper", + "description": "%chatParticipants.pythonHelper.description%", + "isSticky": true, + "when": "config.python.useEnvironmentsExtension != false" + } + ], "configuration": { "properties": { "python-envs.defaultEnvManager": { @@ -586,6 +595,8 @@ "fs-extra": "^11.2.0", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", - "which": "^4.0.0" + "which": "^4.0.0", + "@vscode/chat-extension-utils": "^0.0.0-alpha.1", + "@vscode/prompt-tsx": "^0.3.0-alpha.12" } } diff --git a/package.nls.json b/package.nls.json index f365cfa4..e5ff01e1 100644 --- a/package.nls.json +++ b/package.nls.json @@ -36,5 +36,6 @@ "python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal", "python-envs.uninstallPackage.title": "Uninstall Package", "python-envs.revealProjectInExplorer.title": "Reveal Project in Explorer", - "python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal" + "python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal", + "chatParticipants.pythonHelper.description": "EXPERIMENTAL: Use natural language to learn about Python environments and diagnose issues." } diff --git a/src/extension.ts b/src/extension.ts index 76944e93..0fd016a7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,6 +17,7 @@ import { onDidChangeTerminalShellIntegration, } from './common/window.apis'; import { getConfiguration } from './common/workspace.apis'; +import { registerChatParticipant } from './features/chat/chatParticipant'; import { createManagerReady } from './features/common/managerReady'; import { AutoFindProjects } from './features/creators/autoFindProjects'; import { ExistingProjects } from './features/creators/existingProjects'; @@ -233,6 +234,7 @@ export async function activate(context: ExtensionContext): Promise { + traceWarn('Python helper chat participant invoked with request:', request); + const userPrompt = request.prompt; + + + const prompt: PromptElementAndProps = { + promptElement: PythonEnvsPrompt, + props: { + title: 'Python Environment', + description: 'Provide information about the Python environment.', + request: { + prompt: '' + userPrompt + ' What is the current Python environment? What packages are installed?', + }, + }, + }; + + const { result } = sendChatParticipantRequest( + request, + chatContext, + { + prompt, + requestJustification: vscode.l10n.t('Tell me about my environment.'), + responseStreamOptions: { + stream, + references: false, + responseText: true, + }, + }, + token, + ); + + return await result; + } + ); + participant.iconPath = new vscode.ThemeIcon('python'); + + // Register using our singleton manager + return PythonHelperChatParticipant.register(participant, _context); + + +} + diff --git a/src/features/chat/prompts.tsx b/src/features/chat/prompts.tsx new file mode 100644 index 00000000..03b3a5ce --- /dev/null +++ b/src/features/chat/prompts.tsx @@ -0,0 +1,94 @@ +import { BasePromptElementProps, PromptElement, PromptSizing, UserMessage } from '@vscode/prompt-tsx'; +import { ChatEnvironmentErrorInfo, ERROR_CHAT_CONTEXT_QUEUE } from './send_prompt'; +import { traceWarn } from '../../common/logging'; + +export interface PythonEnvsPromptProps extends BasePromptElementProps { + title: string; + description?: string; + request: { + prompt: string; + }; + response?: { + content: string; + error?: string; + }; + +} + +export class PythonEnvsPrompt extends PromptElement { + render(_state: void, _sizing: PromptSizing) { + + const isContext = !ERROR_CHAT_CONTEXT_QUEUE.isEmpty(); + if (!isContext) { + traceWarn('No context found for python helper chat participant'); + return; + } + const contextErr: ChatEnvironmentErrorInfo | undefined = ERROR_CHAT_CONTEXT_QUEUE.pop(); + if (!contextErr) { + traceWarn('No context error found for python helper chat participant'); + return; + } + const attemptedPackages = contextErr.attemptedPackages.join(', '); + const packagesBeforeInstall = contextErr.packagesBeforeInstall.join(', '); + console.log(packagesBeforeInstall) + const envString = contextErr.environment.displayName + ' (' + contextErr.environment.environmentPath + ') ' + contextErr.environment.version; + console.log(envString); + + const rawPrompt = this.props.request.prompt .replace(/^@pythonHelper\s*/, ''); + let _errorInfo: ChatEnvironmentErrorInfo | undefined; + try { + _errorInfo = JSON.parse(rawPrompt); + } catch (e) { + // Handle parse error + } + return ( + <> + 🚨 **Package Management Error**##
❗ Error Details
```
{contextErr.errorMessage}
```
Stack Trace
```
{contextErr.stackTrace}
```
## 📝 **Context**
**Attempted Packages:**{attemptedPackages}

**Package Manager:** {contextErr.packageManager}
**Environment:** {envString}
**Packages Before Install:** {packagesBeforeInstall}
## 🛠️ **How to Diagnose & Fix**
1. **Diagnose the error above.**
2. **Suggest a fix.**
3. Use the following tools where applicable:
- `installPythonPackage`: Install a package with a version specification, etc.
-`configurePythonEnvironment`: Create an environment with the given attributes.
- `getPythonEnvironmentInfo`: Get more information on the user setup if needed.
If you see 2 to 3 paths to solve this problem, reply with the two paths like so: `button1: prompt solution` `button2: prompt solution
+ + ) + + //
**Attempted Packages:**{attemptedPackages}
**Package Manager:** {contextErr.packageManager}
**Environment:** {contextErr.environment}
**Packages Before Install:** {contextErr.packagesBeforeInstall}
+ //
**Environment:** {envString}
+ + //
**Attempted Packages:**{attemptedPackages}
**Package Manager:** {contextErr.packageManager}
**Environment:** {contextErr.environment}
**Packages Before Install:** {contextErr.packagesBeforeInstall}
+ // ## 🛠️ **How to Diagnose & Fix** + //
1. **Diagnose the error above.**
2. **Suggest a fix.**
3. Use the following tools where applicable:
- `installPythonPackage`: Install a package with a version specification, etc.
-`configurePythonEnvironment`: Create an environment with the given attributes.
- `getPythonEnvironmentInfo`: Get more information on the user setup if needed.
+// return ( +// <> +// 🚨 **Package Management Error** + +// ## ❗ Error Details +// ``` +// {contextErr.errorMessage} +// ``` + +// +// Stack Trace +// ``` +// {contextErr.stackTrace} +// ``` +// + +// ## 📝 **Context** +// +// - **Attempted Packages:** {contextErr.attemptedPackages.join(', ')} +// - **Package Manager:** {contextErr.packageManager} +// - **Environment:** {contextErr.environment} +// - **Packages Before Install:** {contextErr.packagesBeforeInstall} +// + +// ## 🛠️ **How to Diagnose & Fix** +// +// 1. **Diagnose the error above.** +// 2. **Suggest a fix.** +// 3. Use the following tools where applicable: +// - `installPythonPackage`: Install a package with a version specification, etc. +// - `configurePythonEnvironment`: Create an environment with the given attributes. +// - `getPythonEnvironmentInfo`: Get more information on the user setup if needed. + +// If you see multiple equally valid paths forward, offer the user a choice in the format: +// +// +// ); + } +} \ No newline at end of file diff --git a/src/features/chat/pythonHelperChatParticipant.ts b/src/features/chat/pythonHelperChatParticipant.ts new file mode 100644 index 00000000..5580dc26 --- /dev/null +++ b/src/features/chat/pythonHelperChatParticipant.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode'; + +export class PythonHelperChatParticipant { + private static _instance: PythonHelperChatParticipant | undefined; + public readonly participant: vscode.ChatParticipant; + public readonly context: vscode.ExtensionContext; + + constructor(participant: vscode.ChatParticipant, context: vscode.ExtensionContext) { + this.participant = participant; + this.context = context; + } + + public static getInstance(): PythonHelperChatParticipant | undefined { + return PythonHelperChatParticipant._instance; + } + + private static setInstance(instance: PythonHelperChatParticipant): void { + PythonHelperChatParticipant._instance = instance; + } + + public static register( + participant: vscode.ChatParticipant, + context: vscode.ExtensionContext, + ): PythonHelperChatParticipant { + const instance = new PythonHelperChatParticipant(participant, context); + PythonHelperChatParticipant.setInstance(instance); + return instance; + } +} diff --git a/src/features/chat/send_prompt.ts b/src/features/chat/send_prompt.ts new file mode 100644 index 00000000..22e9d8ed --- /dev/null +++ b/src/features/chat/send_prompt.ts @@ -0,0 +1,198 @@ +// // export async function sendPrompt() { +// // const token: CancellationToken = new CancellationTokenSource().token; + +import { commands, extensions, l10n, ProgressLocation, ProgressOptions, window } from 'vscode'; + +// import { +// CancellationToken, +// ChatContext, +// ChatRequest, +// ChatRequestHandler, +// ChatResponseStream, +// LanguageModelChatMessage, +// } from 'vscode'; + +// // const request: ChatRequest = { +// // prompt: 'What is the weather like today?', +// // command: undefined, +// // references: [], +// // toolReferences: [], +// // toolInvocationToken: [], +// // model: undefined, +// // }; + +// // const chatContext: ChatContext = { +// // history: [], +// // }; + +// // const prompt: PromptElementAndProps = { +// // promptElement: PythonEnvsPrompt, +// // }; + +// // const stream: ChatResponseStream = {}; + +// // const { result } = sendChatParticipantRequest( +// // request, +// // chatContext, +// // { +// // prompt, +// // requestJustification: l10n.t('Tell me about my environment.'), +// // responseStreamOptions: { +// // stream, +// // references: false, +// // responseText: true, +// // }, +// // }, +// // token, +// // ); + +// // return await result; +// // } + +// // export async function sendPrompt2() { +// // // send the request +// // const chatResponse = await request.model.sendRequest(messages, {}, token); + +// // // stream the response +// // for await (const fragment of chatResponse.text) { +// // stream.markdown(fragment); +// // } +// // } + +// const BASE_PROMPT = +// 'You are a helpful code tutor. Your job is to teach the user with simple descriptions and sample code of the concept. Respond with a guided overview of the concept in a series of messages. Do not give the user the answer directly, but guide them to find the answer themselves. If the user asks a non-programming question, politely decline to respond.'; + +// // define a chat handler +// const handler: ChatRequestHandler = async ( +// request: ChatRequest, +// context: ChatContext, +// stream: ChatResponseStream, +// token: CancellationToken, +// ) => { +// // initialize the prompt +// let prompt = BASE_PROMPT; + +// // initialize the messages array with the prompt +// const messages = [LanguageModelChatMessage.User(prompt)]; + +// // add in the user's message +// messages.push(LanguageModelChatMessage.User(request.prompt)); + +// // send the request +// const chatResponse = await request.model.sendRequest(messages, {}, token); + +// // stream the response +// for await (const fragment of chatResponse.text) { +// stream.markdown(fragment); +// } + +// return; +// }; + +const COPILOT_CHAT_EXTENSION_ID = 'github.copilot-chat'; + +function isCopilotChatInstalled(): boolean { + return !!extensions.getExtension(COPILOT_CHAT_EXTENSION_ID); +} +export async function sendPromptIfCopilotChatInstalled(prompt: string): Promise { + const sendPrompt = async () => { + // Artificial delay to work around + // https://github.com/microsoft/vscode-copilot/issues/16541 + const progressOptions: ProgressOptions = { + location: ProgressLocation.Notification, + title: l10n.t('Analyzing your python environment extension logs...'), + cancellable: false, + }; + await window.withProgress(progressOptions, async () => { + await new Promise((resolve) => setTimeout(resolve, 5)); + }); + const abc = await commands.executeCommand('workbench.action.chat.open', { + query: prompt, + mode: 'agent', + }); + if (abc) { + console.log('Chat opened successfully'); + } + }; + // If the user has Copilot Chat installed, assume they are + // logged in and can receive the 'sendToNewChat' command + if (isCopilotChatInstalled()) { + await sendPrompt(); + return; + } +} + +export interface ChatEnvironmentErrorInfo { + errorMessage: string; + stackTrace: string; + attemptedPackages: string[]; + packageManager: string; + environment: { + displayName: string; + environmentPath: string; + version: string; + }; + packagesBeforeInstall: string[]; + tools: string[]; +} + +export class PythonEnvErrorQueue { + private errorQueue: ChatEnvironmentErrorInfo[] = []; + + /** + * Add a new error to the queue + * @param error The error information to add + */ + public push(error: ChatEnvironmentErrorInfo): void { + this.errorQueue.push(error); + } + + /** + * Remove and return the oldest error from the queue + * @returns The oldest error or undefined if queue is empty + */ + public pop(): ChatEnvironmentErrorInfo | undefined { + return this.errorQueue.shift(); + } + + /** + * View the next error without removing it + * @returns The oldest error or undefined if queue is empty + */ + public peek(): ChatEnvironmentErrorInfo | undefined { + return this.errorQueue[0]; + } + + /** + * Get the current number of errors in the queue + * @returns The number of errors in the queue + */ + public size(): number { + return this.errorQueue.length; + } + + /** + * Check if the queue is empty + * @returns true if the queue has no errors, false otherwise + */ + public isEmpty(): boolean { + return this.errorQueue.length === 0; + } + + /** + * Clear all errors from the queue + */ + public clear(): void { + this.errorQueue = []; + } + + /** + * Get all errors currently in the queue without removing them + * @returns Array of all errors in the queue + */ + public getAll(): ChatEnvironmentErrorInfo[] { + return [...this.errorQueue]; + } +} + +export const ERROR_CHAT_CONTEXT_QUEUE = new PythonEnvErrorQueue(); diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index da520279..31b3657d 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -8,6 +8,7 @@ import { ThemeIcon, window, } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; import { DidChangePackagesEventArgs, IconPath, @@ -18,10 +19,15 @@ import { PythonEnvironment, PythonEnvironmentApi, } from '../../api'; +import { PythonHelperChatParticipant } from '../../features/chat/pythonHelperChatParticipant'; +import { + ChatEnvironmentErrorInfo, + ERROR_CHAT_CONTEXT_QUEUE, + sendPromptIfCopilotChatInstalled, +} from '../../features/chat/send_prompt'; +import { getWorkspacePackagesToInstall } from './pipUtils'; import { managePackages, refreshPackages } from './utils'; -import { Disposable } from 'vscode-jsonrpc'; import { VenvManager } from './venvManager'; -import { getWorkspacePackagesToInstall } from './pipUtils'; function getChanges(before: Package[], after: Package[]): { kind: PackageChangeKind; pkg: Package }[] { const changes: { kind: PackageChangeKind; pkg: Package }[] = []; @@ -84,10 +90,12 @@ export class PipPackageManager implements PackageManager, Disposable { cancellable: true, }, async (_progress, token) => { + let beforeP: Package[]; try { - const before = this.packages.get(environment.envId.id) ?? []; + const beforePackages = this.packages.get(environment.envId.id); + beforeP = Array.isArray(beforePackages) ? beforePackages : []; const after = await managePackages(environment, manageOptions, this.api, this, token); - const changes = getChanges(before, after); + const changes = getChanges(beforeP, after); this.packages.set(environment.envId.id, after); this._onDidChangePackages.fire({ environment, manager: this, changes }); } catch (e) { @@ -96,12 +104,19 @@ export class PipPackageManager implements PackageManager, Disposable { } this.log.error('Error managing packages', e); setImmediate(async () => { - const result = await window.showErrorMessage('Error managing packages', 'View Output'); + const result = await window.showErrorMessage( + 'Error managing packages', + 'View Output', + 'Diagnose with AI', + ); if (result === 'View Output') { this.log.show(); + return; + } + if (result === 'Diagnose with AI') { + await this.ifDiagnose(environment, e, options, beforeP); } }); - throw e; } }, ); @@ -135,4 +150,35 @@ export class PipPackageManager implements PackageManager, Disposable { this._onDidChangePackages.dispose(); this.packages.clear(); } + async ifDiagnose( + environment: PythonEnvironment, + e: unknown, + options: PackageManagementOptions, + beforeP: Package[], + ): Promise { + const errorMsg = e instanceof Error ? e.message : String(e); + const stack = e instanceof Error ? e.stack ?? '' : ''; + + const ch = PythonHelperChatParticipant.getInstance(); + if (!ch) { + this.log.error('PythonHelperChatParticipant is not registered'); + return; + } + const chatErrorInfo: ChatEnvironmentErrorInfo = { + errorMessage: errorMsg, + stackTrace: stack, + attemptedPackages: options.install ?? [], + packageManager: this.displayName ?? this.name, + environment: { + displayName: environment.displayName, + environmentPath: environment.environmentPath.fsPath ?? environment.environmentPath.toString(), + version: environment.version, + }, + packagesBeforeInstall: beforeP.map((p) => p.name), + tools: ['installPythonPackage', 'configurePythonEnvironment', 'getPythonEnvironmentInfo'], + }; + ERROR_CHAT_CONTEXT_QUEUE.push(chatErrorInfo); + const prompt2 = `@pythonHelper Please help me diagnose the following error when attempting a package management operation using pip.`; + sendPromptIfCopilotChatInstalled(prompt2); + } } diff --git a/tsconfig.json b/tsconfig.json index f33234f4..cc55ad45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { + "jsx": "react", + "jsxFactory": "vscpp", + "jsxFragmentFactory": "vscppf", "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2020", diff --git a/webpack.config.js b/webpack.config.js index c22ce8a8..9b7ffce4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,12 +25,12 @@ const extensionConfig = { }, resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader - extensions: ['.ts', '.js'] + extensions: ['.ts', '.js', '.tsx'] }, module: { rules: [ { - test: /\.ts$/, + test: /\.([cm]?ts|tsx)$/, exclude: /node_modules/, use: [ { From f960f9f251630f9f9408db885da8649d69aadca7 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 18 Jul 2025 08:30:55 -0700 Subject: [PATCH 2/3] prompt updates --- src/features/chat/prompts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/chat/prompts.tsx b/src/features/chat/prompts.tsx index 03b3a5ce..bb3a5f6b 100644 --- a/src/features/chat/prompts.tsx +++ b/src/features/chat/prompts.tsx @@ -43,7 +43,7 @@ export class PythonEnvsPrompt extends PromptElement } return ( <> - 🚨 **Package Management Error**##
❗ Error Details
```
{contextErr.errorMessage}
```
Stack Trace
```
{contextErr.stackTrace}
```
## 📝 **Context**
**Attempted Packages:**{attemptedPackages}

**Package Manager:** {contextErr.packageManager}
**Environment:** {envString}
**Packages Before Install:** {packagesBeforeInstall}
## 🛠️ **How to Diagnose & Fix**
1. **Diagnose the error above.**
2. **Suggest a fix.**
3. Use the following tools where applicable:
- `installPythonPackage`: Install a package with a version specification, etc.
-`configurePythonEnvironment`: Create an environment with the given attributes.
- `getPythonEnvironmentInfo`: Get more information on the user setup if needed.
If you see 2 to 3 paths to solve this problem, reply with the two paths like so: `button1: prompt solution` `button2: prompt solution
+ 🚨 **Package Management Error**##
❗ Error Details
```
{contextErr.errorMessage}
```
Stack Trace
```
{contextErr.stackTrace}
```
## 📝 **Context**
**Attempted Packages:**{attemptedPackages}

**Package Manager:** {contextErr.packageManager}
**Environment:** {envString}
**Packages Before Install:** {packagesBeforeInstall}
## 🛠️ **How to Diagnose & Fix**
1. **Diagnose the error above.**
2. **Suggest a fix.**
3. Use the following tools where applicable:
- `installPythonPackage`: Install a package with a version specification, etc.
-`configurePythonEnvironment`: Create an environment with the given attributes.
If you see one best path forward, start doing that WITHOUT asking the user again. If you see 2 to 3 paths to solve this problem, reply with the two paths like so: `button1: prompt solution` `button2: prompt solution
) From fffb8f182bc6521ca0977121a833e6da278ac0dd Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:32:37 -0700 Subject: [PATCH 3/3] tool calling loop --- src/features/chat/chatParticipant.tsx | 193 +++++++++++++++++++------- src/features/chat/prompts.tsx | 45 ++++-- src/features/chat/send_prompt.ts | 22 ++- 3 files changed, 187 insertions(+), 73 deletions(-) diff --git a/src/features/chat/chatParticipant.tsx b/src/features/chat/chatParticipant.tsx index 44db3ea8..a1dcb26c 100644 --- a/src/features/chat/chatParticipant.tsx +++ b/src/features/chat/chatParticipant.tsx @@ -1,66 +1,153 @@ - -import { sendChatParticipantRequest } from "@vscode/chat-extension-utils"; -import { traceInfo, traceWarn } from "../../common/logging"; -import * as vscode from "vscode"; -import { PythonEnvsPrompt } from "./prompts"; -import { PromptElementAndProps } from "@vscode/chat-extension-utils/dist/toolsPrompt"; -import { ChatEnvironmentErrorInfo, ERROR_CHAT_CONTEXT_QUEUE } from "./send_prompt"; -import { PythonHelperChatParticipant } from "./pythonHelperChatParticipant"; +import { traceInfo, traceWarn } from '../../common/logging'; +import * as vscode from 'vscode'; +import { PythonEnvsPrompt, PythonEnvsPromptProps } from './prompts'; +import { PythonHelperChatParticipant } from './pythonHelperChatParticipant'; +import { PromptElement, renderPrompt, UserMessage } from '@vscode/prompt-tsx'; +import { LanguageModelTextPart, LanguageModelToolCallPart } from 'vscode'; +import { ToolCallRound, ToolResultMetadata } from '@vscode/chat-extension-utils/dist/toolsPrompt'; export const CHAT_PARTICIPANT_ID = 'python-helper'; export const CHAT_PARTICIPANT_AT_MENTION = `@${CHAT_PARTICIPANT_ID}`; +export function registerChatParticipant(_context: vscode.ExtensionContext) { + traceInfo('Registering python helper chat participant'); // Log registration start + // Define the request handler for the chat participant + const requestHandler: vscode.ChatRequestHandler = async ( + request: vscode.ChatRequest, // prompt made in pip file + chatContext: vscode.ChatContext, // history + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + ) => { + traceWarn('Python helper chat participant invoked with request:', request); // Log request details -export function registerChatParticipant( - _context: vscode.ExtensionContext, -) { - traceInfo('Registering python helper chat participant'); - const participant = vscode.chat.createChatParticipant(CHAT_PARTICIPANT_ID, - async ( - request: vscode.ChatRequest, - chatContext: vscode.ChatContext, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken - ) => { - traceWarn('Python helper chat participant invoked with request:', request); - const userPrompt = request.prompt; - - - const prompt: PromptElementAndProps = { - promptElement: PythonEnvsPrompt, - props: { - title: 'Python Environment', - description: 'Provide information about the Python environment.', - request: { - prompt: '' + userPrompt + ' What is the current Python environment? What packages are installed?', - }, - }, - }; - - const { result } = sendChatParticipantRequest( - request, - chatContext, - { - prompt, - requestJustification: vscode.l10n.t('Tell me about my environment.'), - responseStreamOptions: { - stream, - references: false, - responseText: true, - }, + // gather the available tools + const first100tools = vscode.lm.tools.slice(0, 100); + const tools: vscode.LanguageModelChatTool[] = first100tools.map((tool): vscode.LanguageModelChatTool => { + return { + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema ?? {}, + }; + }); + traceInfo('Tools prepared:', tools); // Log tools + + const userPrompt = request.prompt; + traceInfo('User prompt received:', userPrompt); // Log user prompt + + const refTools = request.toolReferences; + const refToolInvToken = request.toolInvocationToken; + const refRef = request.references; + const refCom = request.command; + const refPrompt = request.prompt; + let model = request.model; + traceInfo('References received:', refTools, refToolInvToken, refRef, refCom, refPrompt, model); // Log references + + // takes the info and creates a prompt using the PythonEnvsPrompt + const result = await renderPrompt( + PythonEnvsPrompt, // Extract the constructor PythonEnvsPromptProps + { + title: 'Python Environment', + description: 'Provide information about the Python environment.', + request: { + prompt: '' + userPrompt + ' What is the current Python environment? What packages are installed?', }, - token, - ); + }, + { modelMaxPromptTokens: model.maxInputTokens }, + model, + ); + + // result of building the prompt + let messages = result.messages; + + const options: vscode.LanguageModelChatRequestOptions = { + justification: 'To make a request to @toolsTSX', + tools: tools, + }; + + const toolReferences = [...request.toolReferences]; + const accumulatedToolResults: Record = {}; + const toolCallRounds: ToolCallRound[] = []; + const runWithTools = async (): Promise => { + const requestedTool = toolReferences.shift(); + + if (requestedTool) { + // NOT WORKING::: If a toolReference is present, force the model to call that tool + options.toolMode = vscode.LanguageModelChatToolMode.Required; + options.tools = vscode.lm.tools.filter((tool) => tool.name === requestedTool.name); + } else { + options.toolMode = undefined; + options.tools = [...tools]; + } + console.log('Requested tool:', requestedTool); // Log requested tool + + // Send the request to the model + const response = await model.sendRequest(messages, options, token); + traceInfo('Chat participant response sent:', response); // Log response + + // Stream the response back to VS Code + let responseStr = ''; + const toolCalls: vscode.LanguageModelToolCallPart[] = []; + + for await (const chunk of response.stream) { + if (chunk instanceof LanguageModelTextPart) { + stream.markdown(chunk.value); + responseStr += chunk.value; // Accumulate the response string + } else if (chunk instanceof LanguageModelToolCallPart) { + // If the response contains vscode.LanguageModelToolCallPart, then you should re-send the prompt with a ToolCall element for each of those. + console.log('TOOL CALL', chunk); + toolCalls.push(chunk); + } + } - return await result; - } - ); + if (toolCalls.length) { + traceInfo('Tool calls detected:', toolCalls); // Log tool calls + + // If the model called any tools, then we do another round- render the prompt with those tool calls (rendering the PromptElements will invoke the tools) + // and include the tool results in the prompt for the next request. + toolCallRounds.push({ + response: responseStr, + toolCalls, + }); + + const result = await renderPrompt( + PythonEnvsPrompt, // Extract the constructor PythonEnvsPromptProps + { + title: 'Python Environment', + description: 'Provide information about the Python environment.', + request: { + prompt: + '' + + userPrompt + + ' What is the current Python environment? What packages are installed?', + }, + }, + { modelMaxPromptTokens: model.maxInputTokens }, + model, + ); + + // result of building the prompt + let messages = result.messages; + const toolResultMetadata = result.metadatas.getAll(ToolResultMetadata); + if (toolResultMetadata?.length) { + // Cache tool results for later, so they can be incorporated into later prompts without calling the tool again + toolResultMetadata.forEach((meta) => (accumulatedToolResults[meta.toolCallId] = meta.result)); + } + + // This loops until the model doesn't want to call any more tools, then the request is done. + return runWithTools(); + } + }; + await runWithTools(); // Ensure tools are run before proceeding + + // end of request handler + }; + + const participant = vscode.chat.createChatParticipant(CHAT_PARTICIPANT_ID, requestHandler); participant.iconPath = new vscode.ThemeIcon('python'); + traceInfo('Chat participant created and registered'); // Log participant creation + // Register using our singleton manager return PythonHelperChatParticipant.register(participant, _context); - - } - diff --git a/src/features/chat/prompts.tsx b/src/features/chat/prompts.tsx index bb3a5f6b..485a9e20 100644 --- a/src/features/chat/prompts.tsx +++ b/src/features/chat/prompts.tsx @@ -17,35 +17,50 @@ export interface PythonEnvsPromptProps extends BasePromptElementProps { export class PythonEnvsPrompt extends PromptElement { render(_state: void, _sizing: PromptSizing) { + traceWarn('Render function called'); // Log when render is invoked + // this.props is PythonEnvsPromptProps const isContext = !ERROR_CHAT_CONTEXT_QUEUE.isEmpty(); - if (!isContext) { - traceWarn('No context found for python helper chat participant'); - return; - } + traceWarn('Context queue is empty:', !isContext); // Log the state of the context queue + + if (!isContext) { + traceWarn('No context found for python helper chat participant'); + return; + } + const contextErr: ChatEnvironmentErrorInfo | undefined = ERROR_CHAT_CONTEXT_QUEUE.pop(); + traceWarn('Popped context error:', contextErr); // Log the popped context error + if (!contextErr) { traceWarn('No context error found for python helper chat participant'); return; } + const attemptedPackages = contextErr.attemptedPackages.join(', '); const packagesBeforeInstall = contextErr.packagesBeforeInstall.join(', '); - console.log(packagesBeforeInstall) + traceWarn('Attempted packages:', attemptedPackages); // Log attempted packages + traceWarn('Packages before install:', packagesBeforeInstall); // Log packages before install + const envString = contextErr.environment.displayName + ' (' + contextErr.environment.environmentPath + ') ' + contextErr.environment.version; - console.log(envString); + traceWarn('Environment string:', envString); // Log environment string const rawPrompt = this.props.request.prompt .replace(/^@pythonHelper\s*/, ''); - let _errorInfo: ChatEnvironmentErrorInfo | undefined; - try { - _errorInfo = JSON.parse(rawPrompt); - } catch (e) { - // Handle parse error - } - return ( - <> + traceWarn('Raw prompt:', rawPrompt); // Log the raw prompt + + // let errorInfo: ChatEnvironmentErrorInfo | undefined; + // try { + // errorInfo = JSON.stringify(rawPrompt); + // traceWarn('Parsed error info:', errorInfo); // Log parsed error info + // } catch (e) { + // console.error('Error parsing raw prompt the prompt:', rawPrompt); + // traceWarn('Error parsing raw prompt:', e); // Log parsing error + // } + const msg = <> 🚨 **Package Management Error**##
❗ Error Details
```
{contextErr.errorMessage}
```
Stack Trace
```
{contextErr.stackTrace}
```
## 📝 **Context**
**Attempted Packages:**{attemptedPackages}

**Package Manager:** {contextErr.packageManager}
**Environment:** {envString}
**Packages Before Install:** {packagesBeforeInstall}
## 🛠️ **How to Diagnose & Fix**
1. **Diagnose the error above.**
2. **Suggest a fix.**
3. Use the following tools where applicable:
- `installPythonPackage`: Install a package with a version specification, etc.
-`configurePythonEnvironment`: Create an environment with the given attributes.
If you see one best path forward, start doing that WITHOUT asking the user again. If you see 2 to 3 paths to solve this problem, reply with the two paths like so: `button1: prompt solution` `button2: prompt solution
- ) + + traceWarn('Returning message final:', msg); // Log the final message to be returned + return msg; //
**Attempted Packages:**{attemptedPackages}
**Package Manager:** {contextErr.packageManager}
**Environment:** {contextErr.environment}
**Packages Before Install:** {contextErr.packagesBeforeInstall}
//
**Environment:** {envString}
diff --git a/src/features/chat/send_prompt.ts b/src/features/chat/send_prompt.ts index 22e9d8ed..3bf1ed61 100644 --- a/src/features/chat/send_prompt.ts +++ b/src/features/chat/send_prompt.ts @@ -2,6 +2,7 @@ // // const token: CancellationToken = new CancellationTokenSource().token; import { commands, extensions, l10n, ProgressLocation, ProgressOptions, window } from 'vscode'; +import { traceInfo, traceWarn } from '../../common/logging'; // import { // CancellationToken, @@ -95,30 +96,41 @@ function isCopilotChatInstalled(): boolean { return !!extensions.getExtension(COPILOT_CHAT_EXTENSION_ID); } export async function sendPromptIfCopilotChatInstalled(prompt: string): Promise { + traceInfo('Checking if Copilot Chat is installed'); // Log check start + const sendPrompt = async () => { - // Artificial delay to work around - // https://github.com/microsoft/vscode-copilot/issues/16541 + traceInfo('Preparing to send prompt to Copilot Chat'); // Log prompt preparation + const progressOptions: ProgressOptions = { location: ProgressLocation.Notification, title: l10n.t('Analyzing your python environment extension logs...'), cancellable: false, }; await window.withProgress(progressOptions, async () => { + traceInfo('Showing progress notification'); // Log progress notification await new Promise((resolve) => setTimeout(resolve, 5)); }); + + traceInfo('Executing chat open command with prompt:', prompt); // Log command execution + const abc = await commands.executeCommand('workbench.action.chat.open', { query: prompt, mode: 'agent', }); + if (abc) { - console.log('Chat opened successfully'); + traceInfo('Chat opened successfully'); // Log success + } else { + traceWarn('Failed to open chat'); // Log failure } }; - // If the user has Copilot Chat installed, assume they are - // logged in and can receive the 'sendToNewChat' command + if (isCopilotChatInstalled()) { + traceInfo('Copilot Chat is installed, sending prompt'); // Log installation check await sendPrompt(); return; + } else { + traceWarn('Copilot Chat is not installed'); // Log absence } }