diff --git a/vscode/package.json b/vscode/package.json index 6a11a9dcd4..8f38a42b03 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -398,7 +398,7 @@ "command": "qsharp-vscode.copilotClear", "category": "QDK", "title": "Clear Quantum Copilot chat", - "enablement": "!qdkCopilotIsBusy", + "enablement": "config.Q#.chat.enabled && !qdkCopilotIsBusy", "icon": "$(clear-all)" }, { @@ -828,6 +828,116 @@ ], "additionalProperties": false } + }, + { + "name": "qsharp-run-program", + "tags": [ + "azure-quantum", + "qsharp", + "qdk" + ], + "toolReferenceName": "qsharpRunProgram", + "displayName": "Run Q# Program", + "modelDescription": "Executes a Q# program. The path to a .qs file must be specified in the `filePath` parameter. A quantum simulator will be run locally. Q# does not provide any CLI tools, so call this tool whenever you need to execute Q#, instead of running any CLI commands. The `shots` parameter controls the number of times to repeat the simulation. If the number of shots is greater than 1, a histogram will be generated, and the results will be displayed in a dedicated panel.", + "canBeReferencedInPrompt": true, + "icon": "./resources/file-icon-light.svg", + "inputSchema": { + "type": "object", + "properties": { + "filePath": { + "type": "string", + "description": "The absolute path to the .qs file. If this file is part of a project, the whole project will be compiled as the program. A .qs file belongs to a project if it resides anywhere under a src/ directory whose parent directory also contains a qsharp.json manifest." + }, + "shots": { + "type": "number", + "description": "The number of times to run the program. Defaults to 1 if not specified.", + "default": 1 + } + }, + "required": [ + "filePath" + ], + "additionalProperties": false + } + }, + { + "name": "qsharp-generate-circuit", + "tags": [ + "azure-quantum", + "qsharp", + "qdk" + ], + "toolReferenceName": "qsharpGenerateCircuit", + "displayName": "Generate Q# Circuit Diagram", + "modelDescription": "Generates a visual circuit diagram for a Q# program. The path to a .qs file must be specified in the `filePath` parameter. This tool will compile the Q# program and generate a quantum circuit diagram that visually represents the quantum operations in the code. If the program contains any dynamic behavior such as comparing measurement results, then the program will be executed using a quantum simulator, and the resulting circuit diagram will simply be a trace of the operations that were actually executed during simulation. The diagram will be displayed in a dedicated circuit panel.", + "canBeReferencedInPrompt": true, + "icon": "./resources/file-icon-light.svg", + "inputSchema": { + "type": "object", + "properties": { + "filePath": { + "type": "string", + "description": "The absolute path to the .qs file. If this file is part of a project, the whole project will be compiled as the program. A .qs file belongs to a project if it resides anywhere under a src/ directory whose parent directory also contains a qsharp.json manifest." + } + }, + "required": [ + "filePath" + ], + "additionalProperties": false + } + }, + { + "name": "qsharp-run-resource-estimator", + "tags": [ + "azure-quantum", + "qsharp", + "qdk" + ], + "toolReferenceName": "qsharpRunResourceEstimator", + "displayName": "Run Q# Resource Estimator", + "modelDescription": "Runs the quantum resource estimator on a Q# program to calculate the required physical resources. The path to a .qs file must be specified in the `filePath` parameter. This tool will analyze the Q# program and generate estimates of the quantum resources required to run the algorithm, such as the number of qubits, T gates, and other quantum operations. Results will be displayed in a dedicated resource estimator panel.", + "canBeReferencedInPrompt": true, + "icon": "./resources/file-icon-light.svg", + "inputSchema": { + "type": "object", + "properties": { + "filePath": { + "type": "string", + "description": "The absolute path to the .qs file. If this file is part of a project, the whole project will be compiled as the program. A .qs file belongs to a project if it resides anywhere under a src/ directory whose parent directory also contains a qsharp.json manifest." + }, + "qubitTypes": { + "type": "array", + "description": "Array of qubit type labels to use for resource estimation. Allowed values:\n- 'qubit_gate_ns_e3': Superconducting/spin qubit with 1e-3 error rate\n- 'qubit_gate_ns_e4': Superconducting/spin qubit with 1e-4 error rate\n- 'qubit_gate_us_e3': Trapped ion qubit with 1e-3 error rate\n- 'qubit_gate_us_e4': Trapped ion qubit with 1e-4 error rate\n- 'qubit_maj_ns_e4 + surface_code': Majorana qubit with 1e-4 error rate (surface code QEC)\n- 'qubit_maj_ns_e6 + surface_code': Majorana qubit with 1e-6 error rate (surface code QEC)\n- 'qubit_maj_ns_e4 + floquet_code': Majorana qubit with 1e-4 error rate (floquet code QEC)\n- 'qubit_maj_ns_e6 + floquet_code': Majorana qubit with 1e-6 error rate (floquet code QEC)", + "items": { + "type": "string", + "enum": [ + "qubit_gate_ns_e3", + "qubit_gate_ns_e4", + "qubit_gate_us_e3", + "qubit_gate_us_e4", + "qubit_maj_ns_e4 + surface_code", + "qubit_maj_ns_e6 + surface_code", + "qubit_maj_ns_e4 + floquet_code", + "qubit_maj_ns_e6 + floquet_code" + ] + }, + "default": [ + "qubit_gate_ns_e3" + ] + }, + "errorBudget": { + "type": "number", + "description": "Error budget for the resource estimation. Must be a number between 0 and 1. Default is 0.001.", + "minimum": 0, + "maximum": 1, + "default": 0.001 + } + }, + "required": [ + "filePath" + ], + "additionalProperties": false + } } ] }, diff --git a/vscode/src/circuit.ts b/vscode/src/circuit.ts index 2c29aaaa0d..9790ee7196 100644 --- a/vscode/src/circuit.ts +++ b/vscode/src/circuit.ts @@ -32,7 +32,7 @@ type CircuitParams = { /** * Result of a circuit generation attempt. */ -type CircuitOrError = { +export type CircuitOrError = { simulated: boolean; } & ( | { @@ -116,7 +116,7 @@ export async function showCircuitCommand( * that means this is a dynamic circuit. We fall back to using the * simulator in this case ("trace" mode), which is slower. */ -async function generateCircuit( +export async function generateCircuit( extensionUri: Uri, params: CircuitParams, ): Promise { diff --git a/vscode/src/debugger/output.ts b/vscode/src/debugger/output.ts index bf180c247e..ddbc408893 100644 --- a/vscode/src/debugger/output.ts +++ b/vscode/src/debugger/output.ts @@ -12,8 +12,11 @@ function formatComplex(real: number, imag: number) { return `${r}${i}`; } -export function createDebugConsoleEventTarget(out: (message: string) => void) { - const eventTarget = new QscEventTarget(false); +export function createDebugConsoleEventTarget( + out: (message: string) => void, + captureEvents: boolean = false, +) { + const eventTarget = new QscEventTarget(captureEvents); eventTarget.addEventListener("Message", (evt) => { out(evt.detail + "\n"); diff --git a/vscode/src/estimate.ts b/vscode/src/estimate.ts new file mode 100644 index 0000000000..19acb09189 --- /dev/null +++ b/vscode/src/estimate.ts @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { log } from "qsharp-lang"; +import * as vscode from "vscode"; +import { window } from "vscode"; +import { loadCompilerWorker } from "./common"; +import { clearCommandDiagnostics } from "./diagnostics"; +import { FullProgramConfig, getActiveProgram } from "./programConfig"; +import { + CommandInvocationType, + EventType, + sendTelemetryEvent, +} from "./telemetry"; +import { getRandomGuid } from "./utils"; +import { getOrCreatePanel, sendMessageToPanel } from "./webviewPanel"; + +const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes + +export async function resourceEstimateCommand( + extensionUri: vscode.Uri, + resource?: vscode.Uri, + expr?: string, +) { + clearCommandDiagnostics(); + const associationId = getRandomGuid(); + sendTelemetryEvent( + EventType.TriggerResourceEstimation, + { associationId, invocationType: CommandInvocationType.Command }, + {}, + ); + + const program = await getActiveProgram(); + if (!program.success) { + throw new Error(program.errorMsg); + } + + const qubitType = await window.showQuickPick(qubitTypeOptions, { + canPickMany: true, + title: "Qubit types", + placeHolder: "Superconducting/spin qubit with 1e-3 error rate", + matchOnDetail: true, + }); + + if (!qubitType) { + return; + } + + // Prompt for error budget (default to 0.001) + const validateErrorBudget = (input: string) => { + const result = parseFloat(input); + if (isNaN(result) || result <= 0.0 || result >= 1.0) { + return "Error budgets must be between 0 and 1"; + } + }; + + const errorBudget = await window.showInputBox({ + value: "0.001", + prompt: "Error budget", + validateInput: validateErrorBudget, + }); + + // abort if the user hits during shots entry + if (errorBudget === undefined) { + return; + } + + const runName = await window.showInputBox({ + title: "Friendly name for run", + value: `${program.programConfig.projectName}`, + }); + if (!runName) { + return; + } + + await executeResourceEstimation( + extensionUri, + runName, + associationId, + program.programConfig, + expr, + qubitType, + parseFloat(errorBudget), + ); +} + +const qubitTypeOptions = [ + { + label: "qubit_gate_ns_e3", + detail: "Superconducting/spin qubit with 1e-3 error rate", + picked: true, + params: { + qubitParams: { name: "qubit_gate_ns_e3" }, + qecScheme: { name: "surface_code" }, + }, + }, + { + label: "qubit_gate_ns_e4", + detail: "Superconducting/spin qubit with 1e-4 error rate", + params: { + qubitParams: { name: "qubit_gate_ns_e4" }, + qecScheme: { name: "surface_code" }, + }, + }, + { + label: "qubit_gate_us_e3", + detail: "Trapped ion qubit with 1e-3 error rate", + params: { + qubitParams: { name: "qubit_gate_us_e3" }, + qecScheme: { name: "surface_code" }, + }, + }, + { + label: "qubit_gate_us_e4", + detail: "Trapped ion qubit with 1e-4 error rate", + params: { + qubitParams: { name: "qubit_gate_us_e4" }, + qecScheme: { name: "surface_code" }, + }, + }, + { + label: "qubit_maj_ns_e4 + surface_code", + detail: "Majorana qubit with 1e-4 error rate (surface code QEC)", + params: { + qubitParams: { name: "qubit_maj_ns_e4" }, + qecScheme: { name: "surface_code" }, + }, + }, + { + label: "qubit_maj_ns_e6 + surface_code", + detail: "Majorana qubit with 1e-6 error rate (surface code QEC)", + params: { + qubitParams: { name: "qubit_maj_ns_e6" }, + qecScheme: { name: "surface_code" }, + }, + }, + { + label: "qubit_maj_ns_e4 + floquet_code", + detail: "Majorana qubit with 1e-4 error rate (floquet code QEC)", + params: { + qubitParams: { name: "qubit_maj_ns_e4" }, + qecScheme: { name: "floquet_code" }, + }, + }, + { + label: "qubit_maj_ns_e6 + floquet_code", + detail: "Majorana qubit with 1e-6 error rate (floquet code QEC)", + params: { + qubitParams: { name: "qubit_maj_ns_e6" }, + qecScheme: { name: "floquet_code" }, + }, + }, +]; + +export function resourceEstimateTool( + extensionUri: vscode.Uri, + programConfig: FullProgramConfig, + qubitTypeLabels: string[], + errorBudget: number, +): Promise { + const selectedQubitTypes = qubitTypeOptions.filter((option) => + qubitTypeLabels.includes(option.label), + ); + + const runName = programConfig.projectName || "ResourceEstimation"; + const associationId = getRandomGuid(); + sendTelemetryEvent( + EventType.TriggerResourceEstimation, + { associationId, invocationType: CommandInvocationType.ChatToolCall }, + {}, + ); + + return executeResourceEstimation( + extensionUri, + runName, + associationId, + programConfig, + undefined, + selectedQubitTypes, + errorBudget, + ); +} + +async function executeResourceEstimation( + extensionUri: vscode.Uri, + runName: string, + associationId: string, + programConfig: FullProgramConfig, + expr: string | undefined, + qubitType: { + label: string; + detail: string; + params: { + qubitParams: { + name: string; + }; + qecScheme: { + name: string; + }; + }; + }[], + errorBudget: number, +): Promise { + const params = qubitType.map((item) => ({ + ...item.params, + errorBudget, + estimateType: "frontier", + })); + + log.info("RE params", params); + + sendMessageToPanel({ panelType: "estimates" }, true, { + calculating: true, + }); + + const estimatePanel = getOrCreatePanel("estimates"); + // Ensure the name is unique + if (estimatePanel.state[runName] !== undefined) { + let idx = 2; + for (;;) { + const newName = `${runName}-${idx}`; + if (estimatePanel.state[newName] === undefined) { + runName = newName; + break; + } + idx++; + } + } + estimatePanel.state[runName] = true; + + // Start the worker, run the code, and send the results to the webview + log.debug("Starting resource estimates worker."); + let timedOut = false; + + const worker = loadCompilerWorker(extensionUri); + const compilerTimeout = setTimeout(() => { + log.info("Compiler timeout. Terminating worker."); + timedOut = true; + worker.terminate(); + }, compilerRunTimeoutMs); + + try { + const start = performance.now(); + sendTelemetryEvent( + EventType.ResourceEstimationStart, + { associationId }, + {}, + ); + const estimatesStr = await worker.getEstimates( + programConfig, + expr ?? "", + JSON.stringify(params), + ); + sendTelemetryEvent( + EventType.ResourceEstimationEnd, + { associationId }, + { timeToCompleteMs: performance.now() - start }, + ); + log.debug("Estimates result", estimatesStr); + + // Should be an array of one ReData object returned + const estimates = JSON.parse(estimatesStr); + + for (const item of estimates) { + // if item doesn't have a status property, it's an error + if (!("status" in item) || item.status !== "success") { + log.error("Estimates error code: ", item.code); + log.error("Estimates error message: ", item.message); + throw item.message; + } + } + + (estimates as Array).forEach( + (item) => (item.jobParams.sharedRunName = runName), + ); + + clearTimeout(compilerTimeout); + + const message = { + calculating: false, + estimates, + }; + sendMessageToPanel({ panelType: "estimates" }, true, message); + + return estimates; + } catch (e: any) { + // Stop the 'calculating' animation + const message = { + calculating: false, + estimates: [], + }; + sendMessageToPanel({ panelType: "estimates" }, false, message); + + if (timedOut) { + // Show a VS Code popup that a timeout occurred + window.showErrorMessage( + "The resource estimation timed out. Please try again.", + ); + } else { + log.error("getEstimates error: ", e.toString()); + throw new Error("Estimating failed with error: " + e.toString()); + } + } finally { + if (!timedOut) { + log.debug("Terminating resource estimates worker."); + worker.terminate(); + } + } +} diff --git a/vscode/src/gh-copilot/qsharpTools.ts b/vscode/src/gh-copilot/qsharpTools.ts new file mode 100644 index 0000000000..a0669ef613 --- /dev/null +++ b/vscode/src/gh-copilot/qsharpTools.ts @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { VSDiagnostic } from "qsharp-lang"; +import vscode from "vscode"; +import { CircuitOrError, generateCircuit } from "../circuit"; +import { loadCompilerWorker, toVsCodeDiagnostic } from "../common"; +import { HistogramData } from "../copilot/shared"; +import { CopilotToolError } from "../copilot/tools"; +import { createDebugConsoleEventTarget } from "../debugger/output"; +import { resourceEstimateTool } from "../estimate"; +import { FullProgramConfig, getProgramForDocument } from "../programConfig"; +import { sendMessageToPanel } from "../webviewPanel.js"; + +/** + * In general, tool calls that deal with Q# should include this project + * info in their output. Since Copilot just passes in a file path, and isn't + * familiar with how we expand the project or how we determine target profile, + * this output will give Copilot context to understand what just happened. + */ +type ProjectInfo = { + project: { + name: string; + targetProfile: string; + }; +}; + +type RunProgramResult = ProjectInfo & + ( + | { + output: string; + result: string | vscode.Diagnostic; + } + | { + histogram: HistogramData; + sampleFailures: vscode.Diagnostic[]; + message: string; + } + ); + +export class QSharpTools { + constructor(private extensionUri: vscode.Uri) {} + + /** + * Implements the `qsharp-run-program` tool call. + */ + async runProgram(input: { + filePath: string; + shots?: number; + }): Promise { + const shots = input.shots ?? 1; + + const program = await this.getProgram(input.filePath); + const programConfig = program.programConfig; + + const output: string[] = []; + let finalHistogram: HistogramData | undefined; + let sampleFailures: vscode.Diagnostic[] = []; + const panelId = programConfig.projectName; + + await this.runQsharp( + programConfig, + shots, + (msg) => { + output.push(msg); + }, + (histogram, failures) => { + finalHistogram = histogram; + const uniqueFailures = new Set(); + sampleFailures = []; + for (const failure of failures) { + const failureKey = `${failure.message}-${failure.range?.start.line}-${failure.range?.start.character}`; + if (!uniqueFailures.has(failureKey)) { + uniqueFailures.add(failureKey); + sampleFailures.push(failure); + } + if (sampleFailures.length === 3) { + break; + } + } + if ( + shots > 1 && + histogram.buckets.filter((b) => b[0] !== "ERROR").length > 0 + ) { + // Display the histogram panel only if we're running multiple shots, + // and we have at least one successful result. + sendMessageToPanel( + { panelType: "histogram", id: panelId }, + true, // reveal the panel + histogram, + ); + } + }, + ); + + const project = { + name: programConfig.projectName, + targetProfile: programConfig.profile, + }; + + if (shots === 1) { + // Return the output and results directly + return { + project, + output: output.join("\n"), + result: + sampleFailures.length > 0 + ? sampleFailures[0] + : (finalHistogram?.buckets[0][0] as string), + }; + } else { + // No output, return the histogram + return { + project, + sampleFailures, + histogram: finalHistogram!, + message: `Results are displayed in the Histogram panel.`, + }; + } + } + + /** + * Implements the `qsharp-generate-circuit` tool call. + */ + async generateCircuit(input: { filePath: string }): Promise< + ProjectInfo & + CircuitOrError & { + message?: string; + } + > { + const program = await this.getProgram(input.filePath); + const programConfig = program.programConfig; + + const circuitOrError = await generateCircuit(this.extensionUri, { + program: programConfig, + }); + + const result = { + project: { + name: programConfig.projectName, + targetProfile: programConfig.profile, + }, + ...circuitOrError, + }; + + if (circuitOrError.result === "success") { + return { + ...result, + message: "Circuit is displayed in the Circuit panel.", + }; + } else { + return { + ...result, + }; + } + } + + /** + * Implements the `qsharp-run-resource-estimator` tool call. + */ + async runResourceEstimator(input: { + filePath: string; + qubitTypes?: string[]; + errorBudget?: number; + }): Promise< + ProjectInfo & { + estimates?: object[]; + message: string; + } + > { + const program = await this.getProgram(input.filePath); + const programConfig = program.programConfig; + + const project = { + name: programConfig.projectName, + targetProfile: programConfig.profile, + }; + + try { + const qubitTypes = input.qubitTypes ?? ["qubit_gate_ns_e3"]; + const errorBudget = input.errorBudget ?? 0.001; + + const estimates = await resourceEstimateTool( + this.extensionUri, + programConfig, + qubitTypes, + errorBudget, + ); + + return { + project, + estimates, + message: "Results are displayed in the resource estimator panel.", + }; + } catch (e) { + throw new CopilotToolError( + "Failed to run resource estimator: " + + (e instanceof Error ? e.message : String(e)), + ); + } + } + + private async getProgram(filePath: string) { + const docUri = vscode.Uri.file(filePath); + + const doc = await vscode.workspace.openTextDocument(docUri); + + const program = await getProgramForDocument(doc); + if (!program.success) { + throw new CopilotToolError( + `Cannot get program for the file ${filePath} . error: ${program.errorMsg}`, + ); + } + return program; + } + + private async runQsharp( + program: FullProgramConfig, + shots: number, + out: (message: string) => void, + resultUpdate: ( + histogram: HistogramData, + failures: vscode.Diagnostic[], + ) => void, + ) { + let histogram: HistogramData | undefined; + const evtTarget = createDebugConsoleEventTarget((msg) => { + out(msg); + }, true /* captureEvents */); + + // create a promise that we'll resolve when the run is done + let resolvePromise: () => void = () => {}; + const allShotsDone = new Promise((resolve) => { + resolvePromise = resolve; + }); + + evtTarget.addEventListener("uiResultsRefresh", () => { + const results = evtTarget.getResults(); + const resultCount = evtTarget.resultCount(); // compiler errors come through here too + const buckets = new Map(); + const failures = []; + for (let i = 0; i < resultCount; ++i) { + const key = results[i].result; + const strKey = typeof key !== "string" ? "ERROR" : key; + const newValue = (buckets.get(strKey) || 0) + 1; + buckets.set(strKey, newValue); + if (!results[i].success) { + failures.push(toVsCodeDiagnostic(results[i].result as VSDiagnostic)); + } + } + histogram = { + buckets: Array.from(buckets.entries()), + shotCount: resultCount, + }; + resultUpdate(histogram, failures); + if (shots === resultCount || failures.length > 0) { + // TODO: ugh + resolvePromise(); + } + }); + + const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes + const compilerTimeout = setTimeout(() => { + worker.terminate(); + }, compilerRunTimeoutMs); + const worker = loadCompilerWorker(this.extensionUri!); + + try { + await worker.run(program, "", shots, evtTarget); + // We can still receive events after the above call is done + await allShotsDone; + } catch { + // Compiler errors can come through here. But the error object here doesn't contain enough + // information to be useful. So wait for the one that comes through the event target. + await allShotsDone; + + const failures = evtTarget + .getResults() + .filter((result) => !result.success) + .map((result) => toVsCodeDiagnostic(result.result as VSDiagnostic)); + + throw new CopilotToolError( + `Program failed with compilation errors. ${JSON.stringify(failures)}`, + ); + } + clearTimeout(compilerTimeout); + worker.terminate(); + } +} diff --git a/vscode/src/gh-copilot/tools.ts b/vscode/src/gh-copilot/tools.ts index 88cc81e7fa..06166861b0 100644 --- a/vscode/src/gh-copilot/tools.ts +++ b/vscode/src/gh-copilot/tools.ts @@ -1,11 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { log } from "qsharp-lang"; import * as vscode from "vscode"; import * as azqTools from "../copilot/azqTools"; import { ToolState } from "../copilot/tools"; -import { log } from "qsharp-lang"; -import { Target } from "../azure/treeView"; +import { QSharpTools } from "./qsharpTools"; + +// state +const workspaceState: ToolState = {}; +let qsharpTools: QSharpTools | undefined; const toolDefinitions: { name: string; @@ -13,86 +17,83 @@ const toolDefinitions: { confirm?: (input: any) => vscode.PreparedToolInvocation; }[] = [ // match these to the "languageModelTools" entries in package.json - { name: "azure-quantum-get-jobs", tool: getJobs }, - { name: "azure-quantum-get-job", tool: getJob }, - { name: "azure-quantum-connect-to-workspace", tool: connectToWorkspace }, - { name: "azure-quantum-download-job-results", tool: downloadJobResults }, - { name: "azure-quantum-get-workspaces", tool: getWorkspaces }, + { + name: "azure-quantum-get-jobs", + tool: async (input) => + (await azqTools.getJobs(workspaceState, input)).result, + }, + { + name: "azure-quantum-get-job", + tool: async (input: { job_id: string }) => + (await azqTools.getJob(workspaceState, input)).result, + }, + { + name: "azure-quantum-connect-to-workspace", + tool: async () => + (await azqTools.connectToWorkspace(workspaceState)).result, + }, + { + name: "azure-quantum-download-job-results", + tool: async (input: { job_id: string }) => + (await azqTools.downloadJobResults(workspaceState, input)).result, + }, + { + name: "azure-quantum-get-workspaces", + tool: async () => (await azqTools.getWorkspaces()).result, + }, { name: "azure-quantum-submit-to-target", - tool: submitToTarget, - confirm: submitToTargetConfirm, + tool: async (input: { + job_name: string; + target_id: string; + number_of_shots: number; + }) => (await azqTools.submitToTarget(workspaceState, input, false)).result, + confirm: (input: { + job_name: string; + target_id: string; + number_of_shots: number; + }): vscode.PreparedToolInvocation => ({ + confirmationMessages: { + title: "Submit Azure Quantum job", + message: `Submit job "${input.job_name}" to ${input.target_id} for ${input.number_of_shots} shots?`, + }, + }), + }, + { + name: "azure-quantum-get-active-workspace", + tool: async () => + (await azqTools.getActiveWorkspace(workspaceState)).result, + }, + { + name: "azure-quantum-set-active-workspace", + tool: async (input: { workspace_id: string }) => + (await azqTools.setActiveWorkspace(workspaceState, input)).result, + }, + { + name: "azure-quantum-get-providers", + tool: async () => (await azqTools.getProviders(workspaceState)).result, + }, + { + name: "azure-quantum-get-target", + tool: async (input: { target_id: string }) => + (await azqTools.getTarget(workspaceState, input)).result, + }, + { + name: "qsharp-run-program", + tool: async (input) => await qsharpTools!.runProgram(input), + }, + { + name: "qsharp-generate-circuit", + tool: async (input) => await qsharpTools!.generateCircuit(input), + }, + { + name: "qsharp-run-resource-estimator", + tool: async (input) => await qsharpTools!.runResourceEstimator(input), }, - { name: "azure-quantum-get-active-workspace", tool: getActiveWorkspace }, - { name: "azure-quantum-set-active-workspace", tool: setActiveWorkspace }, - { name: "azure-quantum-get-providers", tool: getProviders }, - { name: "azure-quantum-get-target", tool: getTarget }, ]; -const workspaceState: ToolState = {}; - -async function getJobs(input: { lastNDays: number }): Promise { - return (await azqTools.getJobs(workspaceState, input)).result; -} - -async function getJob(input: { job_id: string }): Promise { - return (await azqTools.getJob(workspaceState, input)).result; -} - -async function connectToWorkspace(): Promise { - return (await azqTools.connectToWorkspace(workspaceState)).result; -} - -async function downloadJobResults(input: { job_id: string }): Promise { - return (await azqTools.downloadJobResults(workspaceState, input)).result; -} - -async function getWorkspaces(): Promise { - return (await azqTools.getWorkspaces()).result; -} - -async function submitToTarget(input: { - job_name: string; - target_id: string; - number_of_shots: number; -}): Promise { - return (await azqTools.submitToTarget(workspaceState, input, false)).result; -} - -function submitToTargetConfirm(input: { - job_name: string; - target_id: string; - number_of_shots: number; -}): vscode.PreparedToolInvocation { - return { - confirmationMessages: { - title: "Submit Azure Quantum job", - message: `Submit job "${input.job_name}" to ${input.target_id} for ${input.number_of_shots} shots?`, - }, - }; -} - -async function getActiveWorkspace(): Promise { - return (await azqTools.getActiveWorkspace(workspaceState)).result; -} - -async function setActiveWorkspace(input: { - workspace_id: string; -}): Promise { - return (await azqTools.setActiveWorkspace(workspaceState, input)).result; -} - -async function getProviders(): Promise { - return (await azqTools.getProviders(workspaceState)).result; -} - -async function getTarget(input: { - target_id: string; -}): Promise { - return (await azqTools.getTarget(workspaceState, input)).result; -} - export function registerLanguageModelTools(context: vscode.ExtensionContext) { + qsharpTools = new QSharpTools(context.extensionUri); for (const { name, tool: fn, confirm: confirmFn } of toolDefinitions) { context.subscriptions.push( vscode.lm.registerTool(name, tool(fn, confirmFn)), diff --git a/vscode/src/telemetry.ts b/vscode/src/telemetry.ts index da698c7f0e..3dcdf58a76 100644 --- a/vscode/src/telemetry.ts +++ b/vscode/src/telemetry.ts @@ -195,7 +195,10 @@ type EventTypes = { measurements: { linesOfCode: number }; }; [EventType.TriggerResourceEstimation]: { - properties: { associationId: string }; + properties: { + associationId: string; + invocationType: CommandInvocationType; + }; measurements: Empty; }; [EventType.ResourceEstimationStart]: { @@ -321,6 +324,11 @@ export enum FormatEvent { OnType = "OnType", } +export enum CommandInvocationType { + Command = "Command", + ChatToolCall = "ChatToolCall", +} + let reporter: TelemetryReporter | undefined; let userAgentString: string | undefined; diff --git a/vscode/src/webviewPanel.ts b/vscode/src/webviewPanel.ts index 2106966537..3fcbe21ce8 100644 --- a/vscode/src/webviewPanel.ts +++ b/vscode/src/webviewPanel.ts @@ -27,6 +27,7 @@ import { EventType, sendTelemetryEvent } from "./telemetry"; import { getRandomGuid } from "./utils"; import { getPauliNoiseModel } from "./config"; import { qsharpExtensionId } from "./common"; +import { resourceEstimateCommand } from "./estimate"; const QSharpWebViewType = "qsharp-webview"; const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes @@ -44,235 +45,11 @@ export function registerWebViewCommands(context: ExtensionContext) { "./out/compilerWorker.js", ).toString(); - const handleShowEstimates = async (resource?: vscode.Uri, expr?: string) => { - clearCommandDiagnostics(); - const associationId = getRandomGuid(); - sendTelemetryEvent( - EventType.TriggerResourceEstimation, - { associationId }, - {}, - ); - const program = await getActiveProgram(); - if (!program.success) { - throw new Error(program.errorMsg); - } - - const qubitType = await window.showQuickPick( - [ - { - label: "qubit_gate_ns_e3", - detail: "Superconducting/spin qubit with 1e-3 error rate", - picked: true, - params: { - qubitParams: { name: "qubit_gate_ns_e3" }, - qecScheme: { name: "surface_code" }, - }, - }, - { - label: "qubit_gate_ns_e4", - detail: "Superconducting/spin qubit with 1e-4 error rate", - params: { - qubitParams: { name: "qubit_gate_ns_e4" }, - qecScheme: { name: "surface_code" }, - }, - }, - { - label: "qubit_gate_us_e3", - detail: "Trapped ion qubit with 1e-3 error rate", - params: { - qubitParams: { name: "qubit_gate_us_e3" }, - qecScheme: { name: "surface_code" }, - }, - }, - { - label: "qubit_gate_us_e4", - detail: "Trapped ion qubit with 1e-4 error rate", - params: { - qubitParams: { name: "qubit_gate_us_e4" }, - qecScheme: { name: "surface_code" }, - }, - }, - { - label: "qubit_maj_ns_e4 + surface_code", - detail: "Majorana qubit with 1e-4 error rate (surface code QEC)", - params: { - qubitParams: { name: "qubit_maj_ns_e4" }, - qecScheme: { name: "surface_code" }, - }, - }, - { - label: "qubit_maj_ns_e6 + surface_code", - detail: "Majorana qubit with 1e-6 error rate (surface code QEC)", - params: { - qubitParams: { name: "qubit_maj_ns_e6" }, - qecScheme: { name: "surface_code" }, - }, - }, - { - label: "qubit_maj_ns_e4 + floquet_code", - detail: "Majorana qubit with 1e-4 error rate (floquet code QEC)", - params: { - qubitParams: { name: "qubit_maj_ns_e4" }, - qecScheme: { name: "floquet_code" }, - }, - }, - { - label: "qubit_maj_ns_e6 + floquet_code", - detail: "Majorana qubit with 1e-6 error rate (floquet code QEC)", - params: { - qubitParams: { name: "qubit_maj_ns_e6" }, - qecScheme: { name: "floquet_code" }, - }, - }, - ], - { - canPickMany: true, - title: "Qubit types", - placeHolder: "Superconducting/spin qubit with 1e-3 error rate", - matchOnDetail: true, - }, - ); - - if (!qubitType) { - return; - } - - // Prompt for error budget (default to 0.001) - const validateErrorBudget = (input: string) => { - const result = parseFloat(input); - if (isNaN(result) || result <= 0.0 || result >= 1.0) { - return "Error budgets must be between 0 and 1"; - } - }; - - const errorBudget = await window.showInputBox({ - value: "0.001", - prompt: "Error budget", - validateInput: validateErrorBudget, - }); - - // abort if the user hits during shots entry - if (errorBudget === undefined) { - return; - } - - let runName = await window.showInputBox({ - title: "Friendly name for run", - value: `${program.programConfig.projectName}`, - }); - if (!runName) { - return; - } - - const params = qubitType.map((item) => ({ - ...item.params, - errorBudget: parseFloat(errorBudget), - estimateType: "frontier", - })); - - log.info("RE params", params); - - sendMessageToPanel({ panelType: "estimates" }, true, { - calculating: true, - }); - - const estimatePanel = getOrCreatePanel("estimates"); - // Ensure the name is unique - if (estimatePanel.state[runName] !== undefined) { - let idx = 2; - for (;;) { - const newName = `${runName}-${idx}`; - if (estimatePanel.state[newName] === undefined) { - runName = newName; - break; - } - idx++; - } - } - estimatePanel.state[runName] = true; - - // Start the worker, run the code, and send the results to the webview - log.debug("Starting resource estimates worker."); - let timedOut = false; - - const worker = getCompilerWorker(compilerWorkerScriptPath); - const compilerTimeout = setTimeout(() => { - log.info("Compiler timeout. Terminating worker."); - timedOut = true; - worker.terminate(); - }, compilerRunTimeoutMs); - - try { - const start = performance.now(); - sendTelemetryEvent( - EventType.ResourceEstimationStart, - { associationId }, - {}, - ); - const estimatesStr = await worker.getEstimates( - program.programConfig, - expr ?? "", - JSON.stringify(params), - ); - sendTelemetryEvent( - EventType.ResourceEstimationEnd, - { associationId }, - { timeToCompleteMs: performance.now() - start }, - ); - log.debug("Estimates result", estimatesStr); - - // Should be an array of one ReData object returned - const estimates = JSON.parse(estimatesStr); - - for (const item of estimates) { - // if item doesn't have a status property, it's an error - if (!("status" in item) || item.status !== "success") { - log.error("Estimates error code: ", item.code); - log.error("Estimates error message: ", item.message); - throw item.message; - } - } - - (estimates as Array).forEach( - (item) => (item.jobParams.sharedRunName = runName), - ); - - clearTimeout(compilerTimeout); - - const message = { - calculating: false, - estimates, - }; - sendMessageToPanel({ panelType: "estimates" }, true, message); - } catch (e: any) { - // Stop the 'calculating' animation - const message = { - calculating: false, - estimates: [], - }; - sendMessageToPanel({ panelType: "estimates" }, false, message); - - if (timedOut) { - // Show a VS Code popup that a timeout occurred - window.showErrorMessage( - "The resource estimation timed out. Please try again.", - ); - } else { - log.error("getEstimates error: ", e.toString()); - throw new Error("Estimating failed with error: " + e.toString()); - } - } finally { - if (!timedOut) { - log.debug("Terminating resource estimates worker."); - worker.terminate(); - } - } - }; - context.subscriptions.push( commands.registerCommand( `${qsharpExtensionId}.showRe`, - handleShowEstimates, + async (resource?: vscode.Uri, expr?: string) => + resourceEstimateCommand(context.extensionUri, resource, expr), ), ); @@ -492,7 +269,7 @@ function createPanel( } } -function getOrCreatePanel(type: PanelType, id?: string): PanelDesc { +export function getOrCreatePanel(type: PanelType, id?: string): PanelDesc { const panel = getPanel(type, id); if (panel) { return panel;