From 0eb29a1682ee09b05b329b0b0b6641c7ba228c35 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 10 Apr 2024 12:40:45 -0700 Subject: [PATCH 01/11] init --- package-lock.json | 10 +- package.json | 2 +- vscode/package.json | 30 +- vscode/src/azure/auth.ts | 1 + vscode/src/chatParticipant.ts | 131 +++++ vscode/src/extension.ts | 4 +- .../vscode.proposed.chatParticipant.d.ts | 480 ++++++++++++++++++ .../vscode.proposed.chatVariableResolver.d.ts | 56 ++ .../vscode.proposed.languageModels.d.ts | 246 +++++++++ 9 files changed, 951 insertions(+), 9 deletions(-) create mode 100644 vscode/src/chatParticipant.ts create mode 100644 vscode/src/typings/vscode.proposed.chatParticipant.d.ts create mode 100644 vscode/src/typings/vscode.proposed.chatVariableResolver.d.ts create mode 100644 vscode/src/typings/vscode.proposed.languageModels.d.ts diff --git a/package-lock.json b/package-lock.json index 8b74c916a9..d6bda6a20f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@types/markdown-it": "^13.0.6", "@types/mocha": "^10.0.2", "@types/node": "^18.17", - "@types/vscode": "^1.83.0", + "@types/vscode": "^1.88.0", "@types/vscode-webview": "^1.57.3", "@typescript-eslint/eslint-plugin": "^7.3.1", "@typescript-eslint/parser": "^7.3.1", @@ -1809,9 +1809,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.87.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.87.0.tgz", - "integrity": "sha512-y3yYJV2esWr8LNjp3VNbSMWG7Y43jC8pCldG8YwiHGAQbsymkkMMt0aDT1xZIOFM2eFcNiUc+dJMx1+Z0UT8fg==", + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.88.0.tgz", + "integrity": "sha512-rWY+Bs6j/f1lvr8jqZTyp5arRMfovdxolcqGi+//+cPDOh8SBvzXH90e7BiSXct5HJ9HGW6jATchbRTpTJpEkw==", "dev": true }, "node_modules/@types/vscode-webview": { @@ -6083,7 +6083,7 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE.txt", "engines": { - "vscode": "^1.77.0" + "vscode": "^1.88.0" } } } diff --git a/package.json b/package.json index a9945e739b..b20883f94b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@types/markdown-it": "^13.0.6", "@types/mocha": "^10.0.2", "@types/node": "^18.17", - "@types/vscode": "^1.83.0", + "@types/vscode": "^1.88.0", "@types/vscode-webview": "^1.57.3", "@typescript-eslint/eslint-plugin": "^7.3.1", "@typescript-eslint/parser": "^7.3.1", diff --git a/vscode/package.json b/vscode/package.json index 77856d69e3..fbc22872c6 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -11,7 +11,7 @@ }, "type": "commonjs", "engines": { - "vscode": "^1.77.0" + "vscode": "^1.88.0" }, "categories": [ "Programming Languages", @@ -27,6 +27,11 @@ "onFileSystem:qsharp-vfs", "onWebviewPanel:qsharp-webview" ], + "enabledApiProposals": [ + "chatParticipant", + "chatVariableResolver", + "languageModels" + ], "contributes": { "walkthroughs": [ { @@ -446,7 +451,28 @@ "[qsharp]": { "debug.saveBeforeStart": "none" } - } + }, + "extensionDependencies": [ + "github.copilot-chat" + ], + "chatParticipants": [ + { + "id": "quantum.chatbot", + "name": "quantum", + "description": "Azure Quantum Copilot", + "isSticky": true, + "commands": [ + { + "name": "samples", + "description": "Show Q# samples" + }, + { + "name": "quantumNotebook", + "description": "Create an Azure Quantum Jupyter Notebook" + } + ] + } + ] }, "scripts": { "build": "npm run tsc:check && node build.mjs", diff --git a/vscode/src/azure/auth.ts b/vscode/src/azure/auth.ts index cca7d5d2fd..af11963cf3 100644 --- a/vscode/src/azure/auth.ts +++ b/vscode/src/azure/auth.ts @@ -12,6 +12,7 @@ import { EndEventProperties } from "./workspaceActions"; export const scopes = { armMgmt: "https://management.azure.com/user_impersonation", quantum: "https://quantum.microsoft.com/user_impersonation", + chatApi: "https://api.quantum.microsoft.com/Chat.ReadWrite", }; export async function getAuthSession( diff --git a/vscode/src/chatParticipant.ts b/vscode/src/chatParticipant.ts new file mode 100644 index 0000000000..ece184e66e --- /dev/null +++ b/vscode/src/chatParticipant.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as vscode from "vscode"; +import { getRandomGuid } from "./utils"; +import { log } from "qsharp-lang"; +import { getAuthSession, scopes } from "./azure/auth"; + +const chatUrl = "https://canary.api.quantum.microsoft.com/api/chat/completions"; +const chatApp = "652066ed-7ea8-4625-a1e9-5bac6600bf06"; + +type quantumChatRequest = { + conversationId: string; // GUID + messages: Array<{ + role: string; // e.g. "user" + content: string; + }>; // The actual question + additionalContext: any; // ? +}; + +type QuantumChatResponse = { + role: string; // e.g. "assistant" + content: string; // The actual answer + embeddedData: any; // ? +}; + +async function chatRequest( + token: string, + question: string, + context?: string, +): Promise { + const payload: quantumChatRequest = { + conversationId: getRandomGuid(), + messages: [ + { + role: "user", + content: question, + }, + ], + additionalContext: { + qcomEnvironment: "Desktop", + }, + }; + + if (context) { + payload.messages.unshift({ + role: "assistant", + content: context, + }); + } + + const options = { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(payload), + }; + log.debug("About to call ChatAPI with payload: ", payload); + + try { + const response = await fetch(chatUrl, options); + log.debug("ChatAPI response status: ", response.statusText); + + const json = await response.json(); + log.debug("ChatAPI response payload: ", json); + return json; + } catch (error) { + log.error("ChatAPI fetch failed with error: ", error); + throw error; + } +} + +const requestHandler: vscode.ChatRequestHandler = async ( + request: vscode.ChatRequest, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, +): Promise => { + const msaChatSession = await getAuthSession( + [scopes.chatApi, `VSCODE_TENANT:common`, `VSCODE_CLIENT_ID:${chatApp}`], + getRandomGuid(), + ); + if (!msaChatSession) throw Error("Failed to get MSA chat token"); + + let response: QuantumChatResponse; + if (request.command == "samples") { + if (request.prompt) { + response = await chatRequest( + msaChatSession.accessToken, + "Please show me the Q# code for " + request.prompt, + ); + } else { + response = await chatRequest( + msaChatSession.accessToken, + "Can you list the names of the quantum samples you could write if asked?", + "The main samples I know how to write are Bell state, Grovers, QRNG, hidden shift, Bernstein-Vazarani, Deutsch-Jozsa, superdense coding, and teleportation", + ); + } + } else if (request.command == "quantumNotebook") { + stream.progress("Opening a new Quantum Notebook..."); + await vscode.commands.executeCommand("qsharp-vscode.createNotebook"); + return Promise.resolve({}); + } else { + response = await chatRequest(msaChatSession.accessToken, request.prompt); + } + if (token.isCancellationRequested) return Promise.reject("Request cancelled"); + + stream.markdown(response.content); + + return Promise.resolve({}); +}; + +export function activateChatParticipant(context: vscode.ExtensionContext) { + const chatbot = vscode.chat.createChatParticipant( + "quantum.chatbot", + requestHandler, + ); + + // Register a chat agent + context.subscriptions.push(chatbot); + + // ToDo: this icon doesn't look great in this context + chatbot.iconPath = vscode.Uri.joinPath( + context.extensionUri, + "resources", + "qdk.png", + ); +} diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 291020f966..8d2a663e59 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -49,6 +49,7 @@ import { } from "./telemetry.js"; import { registerWebViewCommands } from "./webviewPanel.js"; import { initProjectCreator } from "./createProject.js"; +import { activateChatParticipant } from "./chatParticipant.js"; export async function activate( context: vscode.ExtensionContext, @@ -90,6 +91,7 @@ export async function activate( registerWebViewCommands(context); initFileSystem(context); initProjectCreator(context); + activateChatParticipant(context); log.info("Q# extension activated."); @@ -356,7 +358,7 @@ export class QsTextDocumentContentProvider provideTextDocumentContent( uri: vscode.Uri, // eslint-disable-next-line @typescript-eslint/no-unused-vars - token: vscode.CancellationToken, + _token: vscode.CancellationToken, ): vscode.ProviderResult { return getLibrarySourceContent(uri.path); } diff --git a/vscode/src/typings/vscode.proposed.chatParticipant.d.ts b/vscode/src/typings/vscode.proposed.chatParticipant.d.ts new file mode 100644 index 0000000000..7383622fbf --- /dev/null +++ b/vscode/src/typings/vscode.proposed.chatParticipant.d.ts @@ -0,0 +1,480 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + /** + * Represents a user request in chat history. + */ + export class ChatRequestTurn { + + /** + * The prompt as entered by the user. + * + * Information about variables used in this request is stored in {@link ChatRequestTurn.variables}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant and contributing extension to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The variables that were referenced in this message. + */ + readonly variables: ChatResolvedVariable[]; + + private constructor(prompt: string, command: string | undefined, variables: ChatResolvedVariable[], participant: string); + } + + /** + * Represents a chat participant's response in chat history. + */ + export class ChatResponseTurn { + + /** + * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. + */ + readonly response: ReadonlyArray; + + /** + * The result that was received from the chat participant. + */ + readonly result: ChatResult; + + /** + * The id of the chat participant and contributing extension that this response came from. + */ + readonly participant: string; + + /** + * The name of the command that this response came from. + */ + readonly command?: string; + + private constructor(response: ReadonlyArray, result: ChatResult, participant: string); + } + + export interface ChatContext { + /** + * All of the chat messages so far in the current chat session. + */ + readonly history: ReadonlyArray; + } + + /** + * Represents an error result from a chat request. + */ + export interface ChatErrorDetails { + /** + * An error message that is shown to the user. + */ + message: string; + + /** + * If partial markdown content was sent over the {@link ChatRequestHandler handler}'s response stream before the response terminated, then this flag + * can be set to true and it will be rendered with incomplete markdown features patched up. + * + * For example, if the response terminated after sending part of a triple-backtick code block, then the editor will + * render it as a complete code block. + */ + responseIsIncomplete?: boolean; + + /** + * If set to true, the response will be partly blurred out. + */ + responseIsFiltered?: boolean; + } + + /** + * The result of a chat request. + */ + export interface ChatResult { + /** + * If the request resulted in an error, this property defines the error details. + */ + errorDetails?: ChatErrorDetails; + + /** + * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. + */ + readonly metadata?: { readonly [key: string]: any }; + } + + /** + * Represents the type of user feedback received. + */ + export enum ChatResultFeedbackKind { + /** + * The user marked the result as helpful. + */ + Unhelpful = 0, + + /** + * The user marked the result as unhelpful. + */ + Helpful = 1, + } + + /** + * Represents user feedback for a result. + */ + export interface ChatResultFeedback { + /** + * The ChatResult that the user is providing feedback for. + * This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + */ + readonly result: ChatResult; + + /** + * The kind of feedback that was received. + */ + readonly kind: ChatResultFeedbackKind; + } + + /** + * A followup question suggested by the participant. + */ + export interface ChatFollowup { + /** + * The message to send to the chat. + */ + prompt: string; + + /** + * A title to show the user. The prompt will be shown by default, when this is unspecified. + */ + label?: string; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID. + * Followups can only invoke a participant that was contributed by the same extension. + */ + participant?: string; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. + */ + command?: string; + } + + /** + * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. + */ + export interface ChatFollowupProvider { + /** + * Provide followups for the given result. + * @param result This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + * @param token A cancellation token. + */ + provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; + } + + /** + * A chat request handler is a callback that will be invoked when a request is made to a chat participant. + */ + export type ChatRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; + + /** + * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely + * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. + */ + export interface ChatParticipant { + /** + * A unique ID for this participant. + */ + readonly id: string; + + /** + * Icon for the participant shown in UI. + */ + iconPath?: Uri | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } | ThemeIcon; + + /** + * The handler for requests to this participant. + */ + requestHandler: ChatRequestHandler; + + /** + * This provider will be called once after each request to retrieve suggested followup questions. + */ + followupProvider?: ChatFollowupProvider; + + /** + * When the user clicks this participant in `/help`, this text will be submitted to this participant. + */ + sampleRequest?: string; + + /** + * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes + * a result. + * + * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was + * previously returned from this chat participant. + */ + onDidReceiveFeedback: Event; + + /** + * Dispose this participant and free resources + */ + dispose(): void; + } + + /** + * A resolved variable value is a name-value pair as well as the range in the prompt where a variable was used. + */ + export interface ChatResolvedVariable { + /** + * The name of the variable. + * + * *Note* that the name doesn't include the leading `#`-character, + * e.g `selection` for `#selection`. + */ + readonly name: string; + + /** + * The start and end index of the variable in the {@link ChatRequest.prompt prompt}. + * + * *Note* that the indices take the leading `#`-character into account which means they can + * used to modify the prompt as-is. + */ + readonly range?: [start: number, end: number]; + + // TODO@API decouple of resolve API, use `value: string | Uri | (maybe) unknown?` + /** + * The values of the variable. Can be an empty array if the variable doesn't currently have a value. + */ + readonly values: ChatVariableValue[]; + } + + /** + * The location at which the chat is happening. + */ + export enum ChatLocation { + /** + * The chat panel + */ + Panel = 1, + /** + * Terminal inline chat + */ + Terminal = 2, + /** + * Notebook inline chat + */ + Notebook = 3, + /** + * Code editor inline chat + */ + Editor = 4 + } + + export interface ChatRequest { + /** + * The prompt as entered by the user. + * + * Information about variables used in this request is stored in {@link ChatRequest.variables}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command: string | undefined; + + /** + * The list of variables and their values that are referenced in the prompt. + * + * *Note* that the prompt contains varibale references as authored and that it is up to the participant + * to further modify the prompt, for instance by inlining variable values or creating links to + * headings which contain the resolved values. Variables are sorted in reverse by their range + * in the prompt. That means the last variable in the prompt is the first in this list. This simplifies + * string-manipulation of the prompt. + */ + // TODO@API Q? are there implicit variables that are not part of the prompt? + readonly variables: readonly ChatResolvedVariable[]; + + /** + * The location at which the chat is happening. This will always be one of the supported values + */ + readonly location: ChatLocation; + } + + /** + * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content + * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it + * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. + */ + export interface ChatResponseStream { + /** + * Push a markdown part to this stream. Short-hand for + * `push(new ChatResponseMarkdownPart(value))`. + * + * @see {@link ChatResponseStream.push} + * @param value A markdown string or a string that should be interpreted as markdown. + * @returns This stream. + */ + markdown(value: string | MarkdownString): ChatResponseStream; + + /** + * Push an anchor part to this stream. Short-hand for + * `push(new ChatResponseAnchorPart(value, title))`. + * An anchor is an inline reference to some type of resource. + * + * @param value A uri or location + * @param title An optional title that is rendered with value + * @returns This stream. + */ + anchor(value: Uri | Location, title?: string): ChatResponseStream; + + /** + * Push a command button part to this stream. Short-hand for + * `push(new ChatResponseCommandButtonPart(value, title))`. + * + * @param command A Command that will be executed when the button is clicked. + * @returns This stream. + */ + button(command: Command): ChatResponseStream; + + /** + * Push a filetree part to this stream. Short-hand for + * `push(new ChatResponseFileTreePart(value))`. + * + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative to. + * @returns This stream. + */ + filetree(value: ChatResponseFileTree[], baseUri: Uri): ChatResponseStream; + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + * @returns This stream. + */ + progress(value: string): ChatResponseStream; + + /** + * Push a reference to this stream. Short-hand for + * `push(new ChatResponseReferencePart(value))`. + * + * *Note* that the reference is not rendered inline with the response. + * + * @param value A uri or location + * @returns This stream. + */ + reference(value: Uri | Location | { variableName: string; value?: Uri | Location }): ChatResponseStream; + + /** + * Pushes a part to this stream. + * + * @param part A response part, rendered or metadata + */ + push(part: ChatResponsePart): ChatResponseStream; + } + + export class ChatResponseMarkdownPart { + value: MarkdownString; + constructor(value: string | MarkdownString); + } + + export interface ChatResponseFileTree { + name: string; + children?: ChatResponseFileTree[]; + } + + export class ChatResponseFileTreePart { + value: ChatResponseFileTree[]; + baseUri: Uri; + constructor(value: ChatResponseFileTree[], baseUri: Uri); + } + + export class ChatResponseAnchorPart { + value: Uri | Location | SymbolInformation; + title?: string; + constructor(value: Uri | Location | SymbolInformation, title?: string); + } + + export class ChatResponseProgressPart { + value: string; + constructor(value: string); + } + + export class ChatResponseReferencePart { + value: Uri | Location | { variableName: string; value?: Uri | Location }; + constructor(value: Uri | Location | { variableName: string; value?: Uri | Location }); + } + + export class ChatResponseCommandButtonPart { + value: Command; + constructor(value: Command); + } + + /** + * Represents the different chat response types. + */ + export type ChatResponsePart = ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart + | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; + + + export namespace chat { + /** + * Create a new {@link ChatParticipant chat participant} instance. + * + * @param id A unique identifier for the participant. + * @param handler A request handler for the participant. + * @returns A new chat participant + */ + export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant; + } + + /** + * The detail level of this chat variable value. + */ + export enum ChatVariableLevel { + Short = 1, + Medium = 2, + Full = 3 + } + + export interface ChatVariableValue { + /** + * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. + */ + level: ChatVariableLevel; + + /** + * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. + */ + value: string | Uri; + + /** + * A description of this value, which could be provided to the LLM as a hint. + */ + description?: string; + } +} diff --git a/vscode/src/typings/vscode.proposed.chatVariableResolver.d.ts b/vscode/src/typings/vscode.proposed.chatVariableResolver.d.ts new file mode 100644 index 0000000000..eb6f0882d6 --- /dev/null +++ b/vscode/src/typings/vscode.proposed.chatVariableResolver.d.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export namespace chat { + + /** + * Register a variable which can be used in a chat request to any participant. + * @param name The name of the variable, to be used in the chat input as `#name`. + * @param description A description of the variable for the chat input suggest widget. + * @param resolver Will be called to provide the chat variable's value when it is used. + */ + export function registerChatVariableResolver(name: string, description: string, resolver: ChatVariableResolver): Disposable; + } + + export interface ChatVariableValue { + /** + * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. + */ + level: ChatVariableLevel; + + /** + * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. + */ + value: string | Uri; + + /** + * A description of this value, which could be provided to the LLM as a hint. + */ + description?: string; + } + + // TODO@API align with ChatRequest + export interface ChatVariableContext { + /** + * The message entered by the user, which includes this variable. + */ + // TODO@API AS-IS, variables as types, agent/commands stripped + prompt: string; + + // readonly variables: readonly ChatResolvedVariable[]; + } + + export interface ChatVariableResolver { + /** + * A callback to resolve the value of a chat variable. + * @param name The name of the variable. + * @param context Contextual information about this chat request. + * @param token A cancellation token. + */ + resolve(name: string, context: ChatVariableContext, token: CancellationToken): ProviderResult; + } +} diff --git a/vscode/src/typings/vscode.proposed.languageModels.d.ts b/vscode/src/typings/vscode.proposed.languageModels.d.ts new file mode 100644 index 0000000000..98a61ecde4 --- /dev/null +++ b/vscode/src/typings/vscode.proposed.languageModels.d.ts @@ -0,0 +1,246 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + /** + * Represents a language model response. + * + * @see {@link LanguageModelAccess.chatRequest} + */ + export interface LanguageModelChatResponse { + + /** + * An async iterable that is a stream of text chunks forming the overall response. + * + * *Note* that this stream will error when during data receiving an error occurrs. + */ + stream: AsyncIterable; + } + + /** + * A language model message that represents a system message. + * + * System messages provide instructions to the language model that define the context in + * which user messages are interpreted. + * + * *Note* that a language model may choose to add additional system messages to the ones + * provided by extensions. + */ + export class LanguageModelChatSystemMessage { + + /** + * The content of this message. + */ + content: string; + + /** + * Create a new system message. + * + * @param content The content of the message. + */ + constructor(content: string); + } + + /** + * A language model message that represents a user message. + */ + export class LanguageModelChatUserMessage { + + /** + * The content of this message. + */ + content: string; + + /** + * The optional name of a user for this message. + */ + name: string | undefined; + + /** + * Create a new user message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + constructor(content: string, name?: string); + } + + /** + * A language model message that represents an assistant message, usually in response to a user message + * or as a sample response/reply-pair. + */ + export class LanguageModelChatAssistantMessage { + + /** + * The content of this message. + */ + content: string; + + /** + * The optional name of a user for this message. + */ + name: string | undefined; + + /** + * Create a new assistant message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + constructor(content: string, name?: string); + } + + /** + * Different types of language model messages. + */ + export type LanguageModelChatMessage = LanguageModelChatSystemMessage | LanguageModelChatUserMessage | LanguageModelChatAssistantMessage; + + /** + * An event describing the change in the set of available language models. + */ + export interface LanguageModelChangeEvent { + /** + * Added language models. + */ + readonly added: readonly string[]; + /** + * Removed language models. + */ + readonly removed: readonly string[]; + } + + /** + * An error type for language model specific errors. + * + * Consumers of language models should check the code property to determine specific + * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` + * for the case of referring to an unknown language model. For unspecified errors the `cause`-property + * will contain the actual error. + */ + export class LanguageModelError extends Error { + + /** + * The language model does not exist. + */ + static NotFound(message?: string): LanguageModelError; + + /** + * The requestor does not have permissions to use this + * language model + */ + static NoPermissions(message?: string): LanguageModelError; + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like {@linkcode LanguageModelError.NotFound NotFound}, + * or `Unknown` for unspecified errors from the language model itself. In the latter case the + * `cause`-property will contain the actual error. + */ + readonly code: string; + } + + /** + * Options for making a chat request using a language model. + * + * @see {@link lm.chatRequest} + */ + export interface LanguageModelChatRequestOptions { + + /** + * A human-readable message that explains why access to a language model is needed and what feature is enabled by it. + */ + justification?: string; + + /** + * Do not show the consent UI if the user has not yet granted access to the language model but fail the request instead. + */ + // TODO@API Revisit this, how do you do the first request? + silent?: boolean; + + /** + * A set of options that control the behavior of the language model. These options are specific to the language model + * and need to be lookup in the respective documentation. + */ + modelOptions?: { [name: string]: any }; + } + + /** + * Namespace for language model related functionality. + */ + export namespace lm { + + /** + * Make a chat request using a language model. + * + * - *Note 1:* language model use may be subject to access restrictions and user consent. + * + * - *Note 2:* language models are contributed by other extensions and as they evolve and change, + * the set of available language models may change over time. Therefore it is strongly recommend to check + * {@link languageModels} for aviailable values and handle missing language models gracefully. + * + * This function will return a rejected promise if making a request to the language model is not + * possible. Reasons for this can be: + * + * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} + * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} + * - quota limits exceeded, see {@link LanguageModelError.cause `LanguageModelError.cause`} + * + * @param languageModel A language model identifier. + * @param messages An array of message instances. + * @param options Options that control the request. + * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. + */ + export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options: LanguageModelChatRequestOptions, token: CancellationToken): Thenable; + + /** + * The identifiers of all language models that are currently available. + */ + export const languageModels: readonly string[]; + + /** + * An event that is fired when the set of available language models changes. + */ + export const onDidChangeLanguageModels: Event; + } + + /** + * Represents extension specific information about the access to language models. + */ + export interface LanguageModelAccessInformation { + + /** + * An event that fires when access information changes. + */ + onDidChange: Event; + + /** + * Checks if a request can be made to a language model. + * + * *Note* that calling this function will not trigger a consent UI but just checks. + * + * @param languageModelId A language model identifier. + * @return `true` if a request can be made, `false` if not, `undefined` if the language + * model does not exist or consent hasn't been asked for. + */ + canSendRequest(languageModelId: string): boolean | undefined; + + // TODO@API SYNC or ASYNC? + // TODO@API future + // retrieveQuota(languageModelId: string): { remaining: number; resets: Date }; + } + + export interface ExtensionContext { + + /** + * An object that keeps information about how this extension can use language models. + * + * @see {@link lm.sendChatRequest} + */ + readonly languageModelAccessInformation: LanguageModelAccessInformation; + } +} From e4099527964c9b9803aa3cd0f63ac1279c25bcd9 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Thu, 11 Apr 2024 10:17:25 -0700 Subject: [PATCH 02/11] removed unused proposed api's --- vscode/package.json | 4 +- .../vscode.proposed.chatParticipant.d.ts | 27 +- .../vscode.proposed.chatVariableResolver.d.ts | 56 ---- .../vscode.proposed.languageModels.d.ts | 246 ------------------ 4 files changed, 2 insertions(+), 331 deletions(-) rename vscode/src/{typings => proposedApi}/vscode.proposed.chatParticipant.d.ts (97%) delete mode 100644 vscode/src/typings/vscode.proposed.chatVariableResolver.d.ts delete mode 100644 vscode/src/typings/vscode.proposed.languageModels.d.ts diff --git a/vscode/package.json b/vscode/package.json index 5654a62bc3..87a15bfb4c 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -28,9 +28,7 @@ "onWebviewPanel:qsharp-webview" ], "enabledApiProposals": [ - "chatParticipant", - "chatVariableResolver", - "languageModels" + "chatParticipant" ], "contributes": { "walkthroughs": [ diff --git a/vscode/src/typings/vscode.proposed.chatParticipant.d.ts b/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts similarity index 97% rename from vscode/src/typings/vscode.proposed.chatParticipant.d.ts rename to vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts index 7383622fbf..78e0ee870d 100644 --- a/vscode/src/typings/vscode.proposed.chatParticipant.d.ts +++ b/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts @@ -267,28 +267,6 @@ declare module 'vscode' { readonly values: ChatVariableValue[]; } - /** - * The location at which the chat is happening. - */ - export enum ChatLocation { - /** - * The chat panel - */ - Panel = 1, - /** - * Terminal inline chat - */ - Terminal = 2, - /** - * Notebook inline chat - */ - Notebook = 3, - /** - * Code editor inline chat - */ - Editor = 4 - } - export interface ChatRequest { /** * The prompt as entered by the user. @@ -305,6 +283,7 @@ declare module 'vscode' { */ readonly command: string | undefined; + /** * The list of variables and their values that are referenced in the prompt. * @@ -317,10 +296,6 @@ declare module 'vscode' { // TODO@API Q? are there implicit variables that are not part of the prompt? readonly variables: readonly ChatResolvedVariable[]; - /** - * The location at which the chat is happening. This will always be one of the supported values - */ - readonly location: ChatLocation; } /** diff --git a/vscode/src/typings/vscode.proposed.chatVariableResolver.d.ts b/vscode/src/typings/vscode.proposed.chatVariableResolver.d.ts deleted file mode 100644 index eb6f0882d6..0000000000 --- a/vscode/src/typings/vscode.proposed.chatVariableResolver.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - export namespace chat { - - /** - * Register a variable which can be used in a chat request to any participant. - * @param name The name of the variable, to be used in the chat input as `#name`. - * @param description A description of the variable for the chat input suggest widget. - * @param resolver Will be called to provide the chat variable's value when it is used. - */ - export function registerChatVariableResolver(name: string, description: string, resolver: ChatVariableResolver): Disposable; - } - - export interface ChatVariableValue { - /** - * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. - */ - level: ChatVariableLevel; - - /** - * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. - */ - value: string | Uri; - - /** - * A description of this value, which could be provided to the LLM as a hint. - */ - description?: string; - } - - // TODO@API align with ChatRequest - export interface ChatVariableContext { - /** - * The message entered by the user, which includes this variable. - */ - // TODO@API AS-IS, variables as types, agent/commands stripped - prompt: string; - - // readonly variables: readonly ChatResolvedVariable[]; - } - - export interface ChatVariableResolver { - /** - * A callback to resolve the value of a chat variable. - * @param name The name of the variable. - * @param context Contextual information about this chat request. - * @param token A cancellation token. - */ - resolve(name: string, context: ChatVariableContext, token: CancellationToken): ProviderResult; - } -} diff --git a/vscode/src/typings/vscode.proposed.languageModels.d.ts b/vscode/src/typings/vscode.proposed.languageModels.d.ts deleted file mode 100644 index 98a61ecde4..0000000000 --- a/vscode/src/typings/vscode.proposed.languageModels.d.ts +++ /dev/null @@ -1,246 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - /** - * Represents a language model response. - * - * @see {@link LanguageModelAccess.chatRequest} - */ - export interface LanguageModelChatResponse { - - /** - * An async iterable that is a stream of text chunks forming the overall response. - * - * *Note* that this stream will error when during data receiving an error occurrs. - */ - stream: AsyncIterable; - } - - /** - * A language model message that represents a system message. - * - * System messages provide instructions to the language model that define the context in - * which user messages are interpreted. - * - * *Note* that a language model may choose to add additional system messages to the ones - * provided by extensions. - */ - export class LanguageModelChatSystemMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * Create a new system message. - * - * @param content The content of the message. - */ - constructor(content: string); - } - - /** - * A language model message that represents a user message. - */ - export class LanguageModelChatUserMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * The optional name of a user for this message. - */ - name: string | undefined; - - /** - * Create a new user message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - constructor(content: string, name?: string); - } - - /** - * A language model message that represents an assistant message, usually in response to a user message - * or as a sample response/reply-pair. - */ - export class LanguageModelChatAssistantMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * The optional name of a user for this message. - */ - name: string | undefined; - - /** - * Create a new assistant message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - constructor(content: string, name?: string); - } - - /** - * Different types of language model messages. - */ - export type LanguageModelChatMessage = LanguageModelChatSystemMessage | LanguageModelChatUserMessage | LanguageModelChatAssistantMessage; - - /** - * An event describing the change in the set of available language models. - */ - export interface LanguageModelChangeEvent { - /** - * Added language models. - */ - readonly added: readonly string[]; - /** - * Removed language models. - */ - readonly removed: readonly string[]; - } - - /** - * An error type for language model specific errors. - * - * Consumers of language models should check the code property to determine specific - * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` - * for the case of referring to an unknown language model. For unspecified errors the `cause`-property - * will contain the actual error. - */ - export class LanguageModelError extends Error { - - /** - * The language model does not exist. - */ - static NotFound(message?: string): LanguageModelError; - - /** - * The requestor does not have permissions to use this - * language model - */ - static NoPermissions(message?: string): LanguageModelError; - - /** - * A code that identifies this error. - * - * Possible values are names of errors, like {@linkcode LanguageModelError.NotFound NotFound}, - * or `Unknown` for unspecified errors from the language model itself. In the latter case the - * `cause`-property will contain the actual error. - */ - readonly code: string; - } - - /** - * Options for making a chat request using a language model. - * - * @see {@link lm.chatRequest} - */ - export interface LanguageModelChatRequestOptions { - - /** - * A human-readable message that explains why access to a language model is needed and what feature is enabled by it. - */ - justification?: string; - - /** - * Do not show the consent UI if the user has not yet granted access to the language model but fail the request instead. - */ - // TODO@API Revisit this, how do you do the first request? - silent?: boolean; - - /** - * A set of options that control the behavior of the language model. These options are specific to the language model - * and need to be lookup in the respective documentation. - */ - modelOptions?: { [name: string]: any }; - } - - /** - * Namespace for language model related functionality. - */ - export namespace lm { - - /** - * Make a chat request using a language model. - * - * - *Note 1:* language model use may be subject to access restrictions and user consent. - * - * - *Note 2:* language models are contributed by other extensions and as they evolve and change, - * the set of available language models may change over time. Therefore it is strongly recommend to check - * {@link languageModels} for aviailable values and handle missing language models gracefully. - * - * This function will return a rejected promise if making a request to the language model is not - * possible. Reasons for this can be: - * - * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} - * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} - * - quota limits exceeded, see {@link LanguageModelError.cause `LanguageModelError.cause`} - * - * @param languageModel A language model identifier. - * @param messages An array of message instances. - * @param options Options that control the request. - * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. - * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. - */ - export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options: LanguageModelChatRequestOptions, token: CancellationToken): Thenable; - - /** - * The identifiers of all language models that are currently available. - */ - export const languageModels: readonly string[]; - - /** - * An event that is fired when the set of available language models changes. - */ - export const onDidChangeLanguageModels: Event; - } - - /** - * Represents extension specific information about the access to language models. - */ - export interface LanguageModelAccessInformation { - - /** - * An event that fires when access information changes. - */ - onDidChange: Event; - - /** - * Checks if a request can be made to a language model. - * - * *Note* that calling this function will not trigger a consent UI but just checks. - * - * @param languageModelId A language model identifier. - * @return `true` if a request can be made, `false` if not, `undefined` if the language - * model does not exist or consent hasn't been asked for. - */ - canSendRequest(languageModelId: string): boolean | undefined; - - // TODO@API SYNC or ASYNC? - // TODO@API future - // retrieveQuota(languageModelId: string): { remaining: number; resets: Date }; - } - - export interface ExtensionContext { - - /** - * An object that keeps information about how this extension can use language models. - * - * @see {@link lm.sendChatRequest} - */ - readonly languageModelAccessInformation: LanguageModelAccessInformation; - } -} From 91e4a2a48547387917f8fb0f4b5cc760b5c919cb Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Fri, 12 Apr 2024 10:00:19 -0700 Subject: [PATCH 03/11] icon --- vscode/package.json | 2 +- vscode/resources/copilotIcon.png | Bin 0 -> 30542 bytes vscode/src/chatParticipant.ts | 15 +++++++-------- 3 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 vscode/resources/copilotIcon.png diff --git a/vscode/package.json b/vscode/package.json index 87a15bfb4c..b20600f7c3 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -467,7 +467,7 @@ ], "chatParticipants": [ { - "id": "quantum.chatbot", + "id": "quantum.copilot", "name": "quantum", "description": "Azure Quantum Copilot", "isSticky": true, diff --git a/vscode/resources/copilotIcon.png b/vscode/resources/copilotIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..b1afdc06ab76d3a7fb02eb3307d2384e5d068ce1 GIT binary patch literal 30542 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

o4TU+gBTvgse)> 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 Date: Wed, 17 Jul 2024 11:16:58 -0700 Subject: [PATCH 04/11] prettier --- .../vscode.proposed.chatParticipant.d.ts | 935 +++++++++--------- 1 file changed, 486 insertions(+), 449 deletions(-) diff --git a/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts b/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts index 78e0ee870d..524b48f217 100644 --- a/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts +++ b/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts @@ -3,453 +3,490 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare module 'vscode' { - - /** - * Represents a user request in chat history. - */ - export class ChatRequestTurn { - - /** - * The prompt as entered by the user. - * - * Information about variables used in this request is stored in {@link ChatRequestTurn.variables}. - * - * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} - * are not part of the prompt. - */ - readonly prompt: string; - - /** - * The id of the chat participant and contributing extension to which this request was directed. - */ - readonly participant: string; - - /** - * The name of the {@link ChatCommand command} that was selected for this request. - */ - readonly command?: string; - - /** - * The variables that were referenced in this message. - */ - readonly variables: ChatResolvedVariable[]; - - private constructor(prompt: string, command: string | undefined, variables: ChatResolvedVariable[], participant: string); - } - - /** - * Represents a chat participant's response in chat history. - */ - export class ChatResponseTurn { - - /** - * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. - */ - readonly response: ReadonlyArray; - - /** - * The result that was received from the chat participant. - */ - readonly result: ChatResult; - - /** - * The id of the chat participant and contributing extension that this response came from. - */ - readonly participant: string; - - /** - * The name of the command that this response came from. - */ - readonly command?: string; - - private constructor(response: ReadonlyArray, result: ChatResult, participant: string); - } - - export interface ChatContext { - /** - * All of the chat messages so far in the current chat session. - */ - readonly history: ReadonlyArray; - } - - /** - * Represents an error result from a chat request. - */ - export interface ChatErrorDetails { - /** - * An error message that is shown to the user. - */ - message: string; - - /** - * If partial markdown content was sent over the {@link ChatRequestHandler handler}'s response stream before the response terminated, then this flag - * can be set to true and it will be rendered with incomplete markdown features patched up. - * - * For example, if the response terminated after sending part of a triple-backtick code block, then the editor will - * render it as a complete code block. - */ - responseIsIncomplete?: boolean; - - /** - * If set to true, the response will be partly blurred out. - */ - responseIsFiltered?: boolean; - } - - /** - * The result of a chat request. - */ - export interface ChatResult { - /** - * If the request resulted in an error, this property defines the error details. - */ - errorDetails?: ChatErrorDetails; - - /** - * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. - */ - readonly metadata?: { readonly [key: string]: any }; - } - - /** - * Represents the type of user feedback received. - */ - export enum ChatResultFeedbackKind { - /** - * The user marked the result as helpful. - */ - Unhelpful = 0, - - /** - * The user marked the result as unhelpful. - */ - Helpful = 1, - } - - /** - * Represents user feedback for a result. - */ - export interface ChatResultFeedback { - /** - * The ChatResult that the user is providing feedback for. - * This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - */ - readonly result: ChatResult; - - /** - * The kind of feedback that was received. - */ - readonly kind: ChatResultFeedbackKind; - } - - /** - * A followup question suggested by the participant. - */ - export interface ChatFollowup { - /** - * The message to send to the chat. - */ - prompt: string; - - /** - * A title to show the user. The prompt will be shown by default, when this is unspecified. - */ - label?: string; - - /** - * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID. - * Followups can only invoke a participant that was contributed by the same extension. - */ - participant?: string; - - /** - * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. - */ - command?: string; - } - - /** - * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. - */ - export interface ChatFollowupProvider { - /** - * Provide followups for the given result. - * @param result This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - * @param token A cancellation token. - */ - provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; - } - - /** - * A chat request handler is a callback that will be invoked when a request is made to a chat participant. - */ - export type ChatRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; - - /** - * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely - * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. - */ - export interface ChatParticipant { - /** - * A unique ID for this participant. - */ - readonly id: string; - - /** - * Icon for the participant shown in UI. - */ - iconPath?: Uri | { - /** - * The icon path for the light theme. - */ - light: Uri; - /** - * The icon path for the dark theme. - */ - dark: Uri; - } | ThemeIcon; - - /** - * The handler for requests to this participant. - */ - requestHandler: ChatRequestHandler; - - /** - * This provider will be called once after each request to retrieve suggested followup questions. - */ - followupProvider?: ChatFollowupProvider; - - /** - * When the user clicks this participant in `/help`, this text will be submitted to this participant. - */ - sampleRequest?: string; - - /** - * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes - * a result. - * - * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was - * previously returned from this chat participant. - */ - onDidReceiveFeedback: Event; - - /** - * Dispose this participant and free resources - */ - dispose(): void; - } - - /** - * A resolved variable value is a name-value pair as well as the range in the prompt where a variable was used. - */ - export interface ChatResolvedVariable { - /** - * The name of the variable. - * - * *Note* that the name doesn't include the leading `#`-character, - * e.g `selection` for `#selection`. - */ - readonly name: string; - - /** - * The start and end index of the variable in the {@link ChatRequest.prompt prompt}. - * - * *Note* that the indices take the leading `#`-character into account which means they can - * used to modify the prompt as-is. - */ - readonly range?: [start: number, end: number]; - - // TODO@API decouple of resolve API, use `value: string | Uri | (maybe) unknown?` - /** - * The values of the variable. Can be an empty array if the variable doesn't currently have a value. - */ - readonly values: ChatVariableValue[]; - } - - export interface ChatRequest { - /** - * The prompt as entered by the user. - * - * Information about variables used in this request is stored in {@link ChatRequest.variables}. - * - * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} - * are not part of the prompt. - */ - readonly prompt: string; - - /** - * The name of the {@link ChatCommand command} that was selected for this request. - */ - readonly command: string | undefined; - - - /** - * The list of variables and their values that are referenced in the prompt. - * - * *Note* that the prompt contains varibale references as authored and that it is up to the participant - * to further modify the prompt, for instance by inlining variable values or creating links to - * headings which contain the resolved values. Variables are sorted in reverse by their range - * in the prompt. That means the last variable in the prompt is the first in this list. This simplifies - * string-manipulation of the prompt. - */ - // TODO@API Q? are there implicit variables that are not part of the prompt? - readonly variables: readonly ChatResolvedVariable[]; - - } - - /** - * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content - * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it - * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. - */ - export interface ChatResponseStream { - /** - * Push a markdown part to this stream. Short-hand for - * `push(new ChatResponseMarkdownPart(value))`. - * - * @see {@link ChatResponseStream.push} - * @param value A markdown string or a string that should be interpreted as markdown. - * @returns This stream. - */ - markdown(value: string | MarkdownString): ChatResponseStream; - - /** - * Push an anchor part to this stream. Short-hand for - * `push(new ChatResponseAnchorPart(value, title))`. - * An anchor is an inline reference to some type of resource. - * - * @param value A uri or location - * @param title An optional title that is rendered with value - * @returns This stream. - */ - anchor(value: Uri | Location, title?: string): ChatResponseStream; - - /** - * Push a command button part to this stream. Short-hand for - * `push(new ChatResponseCommandButtonPart(value, title))`. - * - * @param command A Command that will be executed when the button is clicked. - * @returns This stream. - */ - button(command: Command): ChatResponseStream; - - /** - * Push a filetree part to this stream. Short-hand for - * `push(new ChatResponseFileTreePart(value))`. - * - * @param value File tree data. - * @param baseUri The base uri to which this file tree is relative to. - * @returns This stream. - */ - filetree(value: ChatResponseFileTree[], baseUri: Uri): ChatResponseStream; - - /** - * Push a progress part to this stream. Short-hand for - * `push(new ChatResponseProgressPart(value))`. - * - * @param value A progress message - * @returns This stream. - */ - progress(value: string): ChatResponseStream; - - /** - * Push a reference to this stream. Short-hand for - * `push(new ChatResponseReferencePart(value))`. - * - * *Note* that the reference is not rendered inline with the response. - * - * @param value A uri or location - * @returns This stream. - */ - reference(value: Uri | Location | { variableName: string; value?: Uri | Location }): ChatResponseStream; - - /** - * Pushes a part to this stream. - * - * @param part A response part, rendered or metadata - */ - push(part: ChatResponsePart): ChatResponseStream; - } - - export class ChatResponseMarkdownPart { - value: MarkdownString; - constructor(value: string | MarkdownString); - } - - export interface ChatResponseFileTree { - name: string; - children?: ChatResponseFileTree[]; - } - - export class ChatResponseFileTreePart { - value: ChatResponseFileTree[]; - baseUri: Uri; - constructor(value: ChatResponseFileTree[], baseUri: Uri); - } - - export class ChatResponseAnchorPart { - value: Uri | Location | SymbolInformation; - title?: string; - constructor(value: Uri | Location | SymbolInformation, title?: string); - } - - export class ChatResponseProgressPart { - value: string; - constructor(value: string); - } - - export class ChatResponseReferencePart { - value: Uri | Location | { variableName: string; value?: Uri | Location }; - constructor(value: Uri | Location | { variableName: string; value?: Uri | Location }); - } - - export class ChatResponseCommandButtonPart { - value: Command; - constructor(value: Command); - } - - /** - * Represents the different chat response types. - */ - export type ChatResponsePart = ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart - | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; - - - export namespace chat { - /** - * Create a new {@link ChatParticipant chat participant} instance. - * - * @param id A unique identifier for the participant. - * @param handler A request handler for the participant. - * @returns A new chat participant - */ - export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant; - } - - /** - * The detail level of this chat variable value. - */ - export enum ChatVariableLevel { - Short = 1, - Medium = 2, - Full = 3 - } - - export interface ChatVariableValue { - /** - * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. - */ - level: ChatVariableLevel; - - /** - * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. - */ - value: string | Uri; - - /** - * A description of this value, which could be provided to the LLM as a hint. - */ - description?: string; - } +declare module "vscode" { + /** + * Represents a user request in chat history. + */ + export class ChatRequestTurn { + /** + * The prompt as entered by the user. + * + * Information about variables used in this request is stored in {@link ChatRequestTurn.variables}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant and contributing extension to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The variables that were referenced in this message. + */ + readonly variables: ChatResolvedVariable[]; + + private constructor( + prompt: string, + command: string | undefined, + variables: ChatResolvedVariable[], + participant: string, + ); + } + + /** + * Represents a chat participant's response in chat history. + */ + export class ChatResponseTurn { + /** + * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. + */ + readonly response: ReadonlyArray< + | ChatResponseMarkdownPart + | ChatResponseFileTreePart + | ChatResponseAnchorPart + | ChatResponseCommandButtonPart + >; + + /** + * The result that was received from the chat participant. + */ + readonly result: ChatResult; + + /** + * The id of the chat participant and contributing extension that this response came from. + */ + readonly participant: string; + + /** + * The name of the command that this response came from. + */ + readonly command?: string; + + private constructor( + response: ReadonlyArray< + | ChatResponseMarkdownPart + | ChatResponseFileTreePart + | ChatResponseAnchorPart + | ChatResponseCommandButtonPart + >, + result: ChatResult, + participant: string, + ); + } + + export interface ChatContext { + /** + * All of the chat messages so far in the current chat session. + */ + readonly history: ReadonlyArray; + } + + /** + * Represents an error result from a chat request. + */ + export interface ChatErrorDetails { + /** + * An error message that is shown to the user. + */ + message: string; + + /** + * If partial markdown content was sent over the {@link ChatRequestHandler handler}'s response stream before the response terminated, then this flag + * can be set to true and it will be rendered with incomplete markdown features patched up. + * + * For example, if the response terminated after sending part of a triple-backtick code block, then the editor will + * render it as a complete code block. + */ + responseIsIncomplete?: boolean; + + /** + * If set to true, the response will be partly blurred out. + */ + responseIsFiltered?: boolean; + } + + /** + * The result of a chat request. + */ + export interface ChatResult { + /** + * If the request resulted in an error, this property defines the error details. + */ + errorDetails?: ChatErrorDetails; + + /** + * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. + */ + readonly metadata?: { readonly [key: string]: any }; + } + + /** + * Represents the type of user feedback received. + */ + export enum ChatResultFeedbackKind { + /** + * The user marked the result as helpful. + */ + Unhelpful = 0, + + /** + * The user marked the result as unhelpful. + */ + Helpful = 1, + } + + /** + * Represents user feedback for a result. + */ + export interface ChatResultFeedback { + /** + * The ChatResult that the user is providing feedback for. + * This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + */ + readonly result: ChatResult; + + /** + * The kind of feedback that was received. + */ + readonly kind: ChatResultFeedbackKind; + } + + /** + * A followup question suggested by the participant. + */ + export interface ChatFollowup { + /** + * The message to send to the chat. + */ + prompt: string; + + /** + * A title to show the user. The prompt will be shown by default, when this is unspecified. + */ + label?: string; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID. + * Followups can only invoke a participant that was contributed by the same extension. + */ + participant?: string; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. + */ + command?: string; + } + + /** + * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. + */ + export interface ChatFollowupProvider { + /** + * Provide followups for the given result. + * @param result This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + * @param token A cancellation token. + */ + provideFollowups( + result: ChatResult, + context: ChatContext, + token: CancellationToken, + ): ProviderResult; + } + + /** + * A chat request handler is a callback that will be invoked when a request is made to a chat participant. + */ + export type ChatRequestHandler = ( + request: ChatRequest, + context: ChatContext, + response: ChatResponseStream, + token: CancellationToken, + ) => ProviderResult; + + /** + * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely + * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. + */ + export interface ChatParticipant { + /** + * A unique ID for this participant. + */ + readonly id: string; + + /** + * Icon for the participant shown in UI. + */ + iconPath?: + | Uri + | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } + | ThemeIcon; + + /** + * The handler for requests to this participant. + */ + requestHandler: ChatRequestHandler; + + /** + * This provider will be called once after each request to retrieve suggested followup questions. + */ + followupProvider?: ChatFollowupProvider; + + /** + * When the user clicks this participant in `/help`, this text will be submitted to this participant. + */ + sampleRequest?: string; + + /** + * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes + * a result. + * + * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was + * previously returned from this chat participant. + */ + onDidReceiveFeedback: Event; + + /** + * Dispose this participant and free resources + */ + dispose(): void; + } + + /** + * A resolved variable value is a name-value pair as well as the range in the prompt where a variable was used. + */ + export interface ChatResolvedVariable { + /** + * The name of the variable. + * + * *Note* that the name doesn't include the leading `#`-character, + * e.g `selection` for `#selection`. + */ + readonly name: string; + + /** + * The start and end index of the variable in the {@link ChatRequest.prompt prompt}. + * + * *Note* that the indices take the leading `#`-character into account which means they can + * used to modify the prompt as-is. + */ + readonly range?: [start: number, end: number]; + + // TODO@API decouple of resolve API, use `value: string | Uri | (maybe) unknown?` + /** + * The values of the variable. Can be an empty array if the variable doesn't currently have a value. + */ + readonly values: ChatVariableValue[]; + } + + export interface ChatRequest { + /** + * The prompt as entered by the user. + * + * Information about variables used in this request is stored in {@link ChatRequest.variables}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command: string | undefined; + + /** + * The list of variables and their values that are referenced in the prompt. + * + * *Note* that the prompt contains varibale references as authored and that it is up to the participant + * to further modify the prompt, for instance by inlining variable values or creating links to + * headings which contain the resolved values. Variables are sorted in reverse by their range + * in the prompt. That means the last variable in the prompt is the first in this list. This simplifies + * string-manipulation of the prompt. + */ + // TODO@API Q? are there implicit variables that are not part of the prompt? + readonly variables: readonly ChatResolvedVariable[]; + } + + /** + * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content + * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it + * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. + */ + export interface ChatResponseStream { + /** + * Push a markdown part to this stream. Short-hand for + * `push(new ChatResponseMarkdownPart(value))`. + * + * @see {@link ChatResponseStream.push} + * @param value A markdown string or a string that should be interpreted as markdown. + * @returns This stream. + */ + markdown(value: string | MarkdownString): ChatResponseStream; + + /** + * Push an anchor part to this stream. Short-hand for + * `push(new ChatResponseAnchorPart(value, title))`. + * An anchor is an inline reference to some type of resource. + * + * @param value A uri or location + * @param title An optional title that is rendered with value + * @returns This stream. + */ + anchor(value: Uri | Location, title?: string): ChatResponseStream; + + /** + * Push a command button part to this stream. Short-hand for + * `push(new ChatResponseCommandButtonPart(value, title))`. + * + * @param command A Command that will be executed when the button is clicked. + * @returns This stream. + */ + button(command: Command): ChatResponseStream; + + /** + * Push a filetree part to this stream. Short-hand for + * `push(new ChatResponseFileTreePart(value))`. + * + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative to. + * @returns This stream. + */ + filetree(value: ChatResponseFileTree[], baseUri: Uri): ChatResponseStream; + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + * @returns This stream. + */ + progress(value: string): ChatResponseStream; + + /** + * Push a reference to this stream. Short-hand for + * `push(new ChatResponseReferencePart(value))`. + * + * *Note* that the reference is not rendered inline with the response. + * + * @param value A uri or location + * @returns This stream. + */ + reference( + value: Uri | Location | { variableName: string; value?: Uri | Location }, + ): ChatResponseStream; + + /** + * Pushes a part to this stream. + * + * @param part A response part, rendered or metadata + */ + push(part: ChatResponsePart): ChatResponseStream; + } + + export class ChatResponseMarkdownPart { + value: MarkdownString; + constructor(value: string | MarkdownString); + } + + export interface ChatResponseFileTree { + name: string; + children?: ChatResponseFileTree[]; + } + + export class ChatResponseFileTreePart { + value: ChatResponseFileTree[]; + baseUri: Uri; + constructor(value: ChatResponseFileTree[], baseUri: Uri); + } + + export class ChatResponseAnchorPart { + value: Uri | Location | SymbolInformation; + title?: string; + constructor(value: Uri | Location | SymbolInformation, title?: string); + } + + export class ChatResponseProgressPart { + value: string; + constructor(value: string); + } + + export class ChatResponseReferencePart { + value: Uri | Location | { variableName: string; value?: Uri | Location }; + constructor( + value: Uri | Location | { variableName: string; value?: Uri | Location }, + ); + } + + export class ChatResponseCommandButtonPart { + value: Command; + constructor(value: Command); + } + + /** + * Represents the different chat response types. + */ + export type ChatResponsePart = + | ChatResponseMarkdownPart + | ChatResponseFileTreePart + | ChatResponseAnchorPart + | ChatResponseProgressPart + | ChatResponseReferencePart + | ChatResponseCommandButtonPart; + + export namespace chat { + /** + * Create a new {@link ChatParticipant chat participant} instance. + * + * @param id A unique identifier for the participant. + * @param handler A request handler for the participant. + * @returns A new chat participant + */ + export function createChatParticipant( + id: string, + handler: ChatRequestHandler, + ): ChatParticipant; + } + + /** + * The detail level of this chat variable value. + */ + export enum ChatVariableLevel { + Short = 1, + Medium = 2, + Full = 3, + } + + export interface ChatVariableValue { + /** + * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. + */ + level: ChatVariableLevel; + + /** + * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. + */ + value: string | Uri; + + /** + * A description of this value, which could be provided to the LLM as a hint. + */ + description?: string; + } } From 24610678b1603597616b661eeb864a559783d868 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 17 Jul 2024 11:44:00 -0700 Subject: [PATCH 05/11] remove proposed api file and use built-in api --- package-lock.json | 10 +- package.json | 2 +- vscode/package.json | 5 +- .../vscode.proposed.chatParticipant.d.ts | 492 ------------------ 4 files changed, 7 insertions(+), 502 deletions(-) delete mode 100644 vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts diff --git a/package-lock.json b/package-lock.json index fb02313ccb..3f48846d22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@types/markdown-it": "^14.1.1", "@types/mocha": "^10.0.2", "@types/node": "^18.17", - "@types/vscode": "^1.88.0", + "@types/vscode": "^1.90.0", "@types/vscode-webview": "^1.57.3", "@typescript-eslint/eslint-plugin": "^7.3.1", "@typescript-eslint/parser": "^7.3.1", @@ -1807,9 +1807,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.88.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.88.0.tgz", - "integrity": "sha512-rWY+Bs6j/f1lvr8jqZTyp5arRMfovdxolcqGi+//+cPDOh8SBvzXH90e7BiSXct5HJ9HGW6jATchbRTpTJpEkw==", + "version": "1.91.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.91.0.tgz", + "integrity": "sha512-PgPr+bUODjG3y+ozWUCyzttqR9EHny9sPAfJagddQjDwdtf66y2sDKJMnFZRuzBA2YtBGASqJGPil8VDUPvO6A==", "dev": true }, "node_modules/@types/vscode-webview": { @@ -6073,7 +6073,7 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE.txt", "engines": { - "vscode": "^1.88.0" + "vscode": "^1.90.0" } } } diff --git a/package.json b/package.json index f3212bea23..0894fc9dea 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@types/markdown-it": "^14.1.1", "@types/mocha": "^10.0.2", "@types/node": "^18.17", - "@types/vscode": "^1.88.0", + "@types/vscode": "^1.90.0", "@types/vscode-webview": "^1.57.3", "@typescript-eslint/eslint-plugin": "^7.3.1", "@typescript-eslint/parser": "^7.3.1", diff --git a/vscode/package.json b/vscode/package.json index 8faa01ecb4..fe656e9420 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -11,7 +11,7 @@ }, "type": "commonjs", "engines": { - "vscode": "^1.88.0" + "vscode": "^1.90.0" }, "categories": [ "Programming Languages", @@ -27,9 +27,6 @@ "onFileSystem:qsharp-vfs", "onWebviewPanel:qsharp-webview" ], - "enabledApiProposals": [ - "chatParticipant" - ], "contributes": { "walkthroughs": [ { diff --git a/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts b/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts deleted file mode 100644 index 524b48f217..0000000000 --- a/vscode/src/proposedApi/vscode.proposed.chatParticipant.d.ts +++ /dev/null @@ -1,492 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module "vscode" { - /** - * Represents a user request in chat history. - */ - export class ChatRequestTurn { - /** - * The prompt as entered by the user. - * - * Information about variables used in this request is stored in {@link ChatRequestTurn.variables}. - * - * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} - * are not part of the prompt. - */ - readonly prompt: string; - - /** - * The id of the chat participant and contributing extension to which this request was directed. - */ - readonly participant: string; - - /** - * The name of the {@link ChatCommand command} that was selected for this request. - */ - readonly command?: string; - - /** - * The variables that were referenced in this message. - */ - readonly variables: ChatResolvedVariable[]; - - private constructor( - prompt: string, - command: string | undefined, - variables: ChatResolvedVariable[], - participant: string, - ); - } - - /** - * Represents a chat participant's response in chat history. - */ - export class ChatResponseTurn { - /** - * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. - */ - readonly response: ReadonlyArray< - | ChatResponseMarkdownPart - | ChatResponseFileTreePart - | ChatResponseAnchorPart - | ChatResponseCommandButtonPart - >; - - /** - * The result that was received from the chat participant. - */ - readonly result: ChatResult; - - /** - * The id of the chat participant and contributing extension that this response came from. - */ - readonly participant: string; - - /** - * The name of the command that this response came from. - */ - readonly command?: string; - - private constructor( - response: ReadonlyArray< - | ChatResponseMarkdownPart - | ChatResponseFileTreePart - | ChatResponseAnchorPart - | ChatResponseCommandButtonPart - >, - result: ChatResult, - participant: string, - ); - } - - export interface ChatContext { - /** - * All of the chat messages so far in the current chat session. - */ - readonly history: ReadonlyArray; - } - - /** - * Represents an error result from a chat request. - */ - export interface ChatErrorDetails { - /** - * An error message that is shown to the user. - */ - message: string; - - /** - * If partial markdown content was sent over the {@link ChatRequestHandler handler}'s response stream before the response terminated, then this flag - * can be set to true and it will be rendered with incomplete markdown features patched up. - * - * For example, if the response terminated after sending part of a triple-backtick code block, then the editor will - * render it as a complete code block. - */ - responseIsIncomplete?: boolean; - - /** - * If set to true, the response will be partly blurred out. - */ - responseIsFiltered?: boolean; - } - - /** - * The result of a chat request. - */ - export interface ChatResult { - /** - * If the request resulted in an error, this property defines the error details. - */ - errorDetails?: ChatErrorDetails; - - /** - * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. - */ - readonly metadata?: { readonly [key: string]: any }; - } - - /** - * Represents the type of user feedback received. - */ - export enum ChatResultFeedbackKind { - /** - * The user marked the result as helpful. - */ - Unhelpful = 0, - - /** - * The user marked the result as unhelpful. - */ - Helpful = 1, - } - - /** - * Represents user feedback for a result. - */ - export interface ChatResultFeedback { - /** - * The ChatResult that the user is providing feedback for. - * This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - */ - readonly result: ChatResult; - - /** - * The kind of feedback that was received. - */ - readonly kind: ChatResultFeedbackKind; - } - - /** - * A followup question suggested by the participant. - */ - export interface ChatFollowup { - /** - * The message to send to the chat. - */ - prompt: string; - - /** - * A title to show the user. The prompt will be shown by default, when this is unspecified. - */ - label?: string; - - /** - * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID. - * Followups can only invoke a participant that was contributed by the same extension. - */ - participant?: string; - - /** - * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. - */ - command?: string; - } - - /** - * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. - */ - export interface ChatFollowupProvider { - /** - * Provide followups for the given result. - * @param result This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - * @param token A cancellation token. - */ - provideFollowups( - result: ChatResult, - context: ChatContext, - token: CancellationToken, - ): ProviderResult; - } - - /** - * A chat request handler is a callback that will be invoked when a request is made to a chat participant. - */ - export type ChatRequestHandler = ( - request: ChatRequest, - context: ChatContext, - response: ChatResponseStream, - token: CancellationToken, - ) => ProviderResult; - - /** - * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely - * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. - */ - export interface ChatParticipant { - /** - * A unique ID for this participant. - */ - readonly id: string; - - /** - * Icon for the participant shown in UI. - */ - iconPath?: - | Uri - | { - /** - * The icon path for the light theme. - */ - light: Uri; - /** - * The icon path for the dark theme. - */ - dark: Uri; - } - | ThemeIcon; - - /** - * The handler for requests to this participant. - */ - requestHandler: ChatRequestHandler; - - /** - * This provider will be called once after each request to retrieve suggested followup questions. - */ - followupProvider?: ChatFollowupProvider; - - /** - * When the user clicks this participant in `/help`, this text will be submitted to this participant. - */ - sampleRequest?: string; - - /** - * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes - * a result. - * - * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was - * previously returned from this chat participant. - */ - onDidReceiveFeedback: Event; - - /** - * Dispose this participant and free resources - */ - dispose(): void; - } - - /** - * A resolved variable value is a name-value pair as well as the range in the prompt where a variable was used. - */ - export interface ChatResolvedVariable { - /** - * The name of the variable. - * - * *Note* that the name doesn't include the leading `#`-character, - * e.g `selection` for `#selection`. - */ - readonly name: string; - - /** - * The start and end index of the variable in the {@link ChatRequest.prompt prompt}. - * - * *Note* that the indices take the leading `#`-character into account which means they can - * used to modify the prompt as-is. - */ - readonly range?: [start: number, end: number]; - - // TODO@API decouple of resolve API, use `value: string | Uri | (maybe) unknown?` - /** - * The values of the variable. Can be an empty array if the variable doesn't currently have a value. - */ - readonly values: ChatVariableValue[]; - } - - export interface ChatRequest { - /** - * The prompt as entered by the user. - * - * Information about variables used in this request is stored in {@link ChatRequest.variables}. - * - * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} - * are not part of the prompt. - */ - readonly prompt: string; - - /** - * The name of the {@link ChatCommand command} that was selected for this request. - */ - readonly command: string | undefined; - - /** - * The list of variables and their values that are referenced in the prompt. - * - * *Note* that the prompt contains varibale references as authored and that it is up to the participant - * to further modify the prompt, for instance by inlining variable values or creating links to - * headings which contain the resolved values. Variables are sorted in reverse by their range - * in the prompt. That means the last variable in the prompt is the first in this list. This simplifies - * string-manipulation of the prompt. - */ - // TODO@API Q? are there implicit variables that are not part of the prompt? - readonly variables: readonly ChatResolvedVariable[]; - } - - /** - * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content - * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it - * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. - */ - export interface ChatResponseStream { - /** - * Push a markdown part to this stream. Short-hand for - * `push(new ChatResponseMarkdownPart(value))`. - * - * @see {@link ChatResponseStream.push} - * @param value A markdown string or a string that should be interpreted as markdown. - * @returns This stream. - */ - markdown(value: string | MarkdownString): ChatResponseStream; - - /** - * Push an anchor part to this stream. Short-hand for - * `push(new ChatResponseAnchorPart(value, title))`. - * An anchor is an inline reference to some type of resource. - * - * @param value A uri or location - * @param title An optional title that is rendered with value - * @returns This stream. - */ - anchor(value: Uri | Location, title?: string): ChatResponseStream; - - /** - * Push a command button part to this stream. Short-hand for - * `push(new ChatResponseCommandButtonPart(value, title))`. - * - * @param command A Command that will be executed when the button is clicked. - * @returns This stream. - */ - button(command: Command): ChatResponseStream; - - /** - * Push a filetree part to this stream. Short-hand for - * `push(new ChatResponseFileTreePart(value))`. - * - * @param value File tree data. - * @param baseUri The base uri to which this file tree is relative to. - * @returns This stream. - */ - filetree(value: ChatResponseFileTree[], baseUri: Uri): ChatResponseStream; - - /** - * Push a progress part to this stream. Short-hand for - * `push(new ChatResponseProgressPart(value))`. - * - * @param value A progress message - * @returns This stream. - */ - progress(value: string): ChatResponseStream; - - /** - * Push a reference to this stream. Short-hand for - * `push(new ChatResponseReferencePart(value))`. - * - * *Note* that the reference is not rendered inline with the response. - * - * @param value A uri or location - * @returns This stream. - */ - reference( - value: Uri | Location | { variableName: string; value?: Uri | Location }, - ): ChatResponseStream; - - /** - * Pushes a part to this stream. - * - * @param part A response part, rendered or metadata - */ - push(part: ChatResponsePart): ChatResponseStream; - } - - export class ChatResponseMarkdownPart { - value: MarkdownString; - constructor(value: string | MarkdownString); - } - - export interface ChatResponseFileTree { - name: string; - children?: ChatResponseFileTree[]; - } - - export class ChatResponseFileTreePart { - value: ChatResponseFileTree[]; - baseUri: Uri; - constructor(value: ChatResponseFileTree[], baseUri: Uri); - } - - export class ChatResponseAnchorPart { - value: Uri | Location | SymbolInformation; - title?: string; - constructor(value: Uri | Location | SymbolInformation, title?: string); - } - - export class ChatResponseProgressPart { - value: string; - constructor(value: string); - } - - export class ChatResponseReferencePart { - value: Uri | Location | { variableName: string; value?: Uri | Location }; - constructor( - value: Uri | Location | { variableName: string; value?: Uri | Location }, - ); - } - - export class ChatResponseCommandButtonPart { - value: Command; - constructor(value: Command); - } - - /** - * Represents the different chat response types. - */ - export type ChatResponsePart = - | ChatResponseMarkdownPart - | ChatResponseFileTreePart - | ChatResponseAnchorPart - | ChatResponseProgressPart - | ChatResponseReferencePart - | ChatResponseCommandButtonPart; - - export namespace chat { - /** - * Create a new {@link ChatParticipant chat participant} instance. - * - * @param id A unique identifier for the participant. - * @param handler A request handler for the participant. - * @returns A new chat participant - */ - export function createChatParticipant( - id: string, - handler: ChatRequestHandler, - ): ChatParticipant; - } - - /** - * The detail level of this chat variable value. - */ - export enum ChatVariableLevel { - Short = 1, - Medium = 2, - Full = 3, - } - - export interface ChatVariableValue { - /** - * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. - */ - level: ChatVariableLevel; - - /** - * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. - */ - value: string | Uri; - - /** - * A description of this value, which could be provided to the LLM as a hint. - */ - description?: string; - } -} From d5621010072d87ccf244456512e045222fbf4017 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Mon, 22 Jul 2024 14:07:41 -0700 Subject: [PATCH 06/11] use fetch event source to get copilot streaming --- vscode/src/chatParticipant.ts | 50 +++++---- vscode/src/fetch.ts | 91 ++++++++++++++++ vscode/src/parse.ts | 193 ++++++++++++++++++++++++++++++++++ 3 files changed, 316 insertions(+), 18 deletions(-) create mode 100644 vscode/src/fetch.ts create mode 100644 vscode/src/parse.ts diff --git a/vscode/src/chatParticipant.ts b/vscode/src/chatParticipant.ts index c6d7cad717..621b128533 100644 --- a/vscode/src/chatParticipant.ts +++ b/vscode/src/chatParticipant.ts @@ -5,8 +5,10 @@ import * as vscode from "vscode"; import { getRandomGuid } from "./utils"; import { log } from "qsharp-lang"; import { getAuthSession, scopes } from "./azure/auth"; +import { fetchEventSource } from "./fetch"; -const chatUrl = "https://canary.api.quantum.microsoft.com/api/chat/completions"; +const chatUrl = + "https://westus3.aqa.canary.quantum.azure.com/api/chat/streaming"; const chatApp = "652066ed-7ea8-4625-a1e9-5bac6600bf06"; type quantumChatRequest = { @@ -15,20 +17,27 @@ type quantumChatRequest = { role: string; // e.g. "user" content: string; }>; // The actual question - additionalContext: any; // ? + additionalContext: any; + identifier: string; }; type QuantumChatResponse = { - role: string; // e.g. "assistant" - content: string; // The actual answer - embeddedData: any; // ? + ConversationId: string; // GUID, + Role: string; // e.g. "assistant" + Content?: string; // The full response + Delta?: string; // The next response token + FinishReason?: string; // e.g. "stop"|"content_filter"|"length"|null, + EmbeddedData: any; + Created: string; // e.g. "2021-09-29T17:00:00.000Z" }; async function chatRequest( token: string, question: string, + stream: vscode.ChatResponseStream, context?: string, -): Promise { +): Promise { + log.debug("Requesting response"); const payload: quantumChatRequest = { conversationId: getRandomGuid(), messages: [ @@ -40,6 +49,7 @@ async function chatRequest( additionalContext: { qcomEnvironment: "Desktop", }, + identifier: "Quantum", }; if (context) { @@ -57,15 +67,19 @@ async function chatRequest( }, body: JSON.stringify(payload), }; - log.debug("About to call ChatAPI with payload: ", payload); try { - const response = await fetch(chatUrl, options); - log.debug("ChatAPI response status: ", response.statusText); + log.debug("About to call ChatAPI with payload: ", payload); + await fetchEventSource(chatUrl, { + ...options, + onMessage(ev) { + const messageReceived: QuantumChatResponse = JSON.parse(ev.data); + log.debug("Received message: ", messageReceived); + if (messageReceived.Delta) stream.markdown(messageReceived.Delta); + }, + }); - const json = await response.json(); - log.debug("ChatAPI response payload: ", json); - return json; + return Promise.resolve({}); } catch (error) { log.error("ChatAPI fetch failed with error: ", error); throw error; @@ -85,17 +99,19 @@ const requestHandler: vscode.ChatRequestHandler = async ( ); if (!msaChatSession) throw Error("Failed to get MSA chat token"); - let response: QuantumChatResponse; + //let response: QuantumChatResponse; if (request.command == "samples") { if (request.prompt) { - response = await chatRequest( + await chatRequest( msaChatSession.accessToken, "Please show me the Q# code for " + request.prompt, + stream, ); } else { - response = await chatRequest( + await chatRequest( msaChatSession.accessToken, "Can you list the names of the quantum samples you could write if asked?", + stream, "The main samples I know how to write are Bell state, Grovers, QRNG, hidden shift, Bernstein-Vazarani, Deutsch-Jozsa, superdense coding, and teleportation", ); } @@ -104,12 +120,10 @@ const requestHandler: vscode.ChatRequestHandler = async ( await vscode.commands.executeCommand("qsharp-vscode.createNotebook"); return Promise.resolve({}); } else { - response = await chatRequest(msaChatSession.accessToken, request.prompt); + await chatRequest(msaChatSession.accessToken, request.prompt, stream); } if (token.isCancellationRequested) return Promise.reject("Request cancelled"); - stream.markdown(response.content); - return Promise.resolve({}); }; diff --git a/vscode/src/fetch.ts b/vscode/src/fetch.ts new file mode 100644 index 0000000000..1886f721d9 --- /dev/null +++ b/vscode/src/fetch.ts @@ -0,0 +1,91 @@ +import { EventSourceMessage, getBytes, getLines, getMessages } from "./parse"; + +export const EventStreamContentType = "text/event-stream"; + +const LastEventId = "last-event-id"; + +export interface FetchEventSourceInit extends RequestInit { + /** + * The request headers. FetchEventSource only supports the Record format. + */ + headers?: Record; + + /** + * Called when a message is received. NOTE: Unlike the default browser + * EventSource.onmessage, this callback is called for _all_ events, + * even ones with a custom `event` field. + */ + onMessage?: (ev: EventSourceMessage) => void; +} + +export function fetchEventSource( + input: RequestInfo, + { + headers: inputHeaders, + onMessage: onMessage, + ...rest + }: FetchEventSourceInit, +) { + return new Promise((resolve, reject) => { + // make a copy of the input headers since we may modify it below: + const headers = { ...inputHeaders }; + if (!headers.accept) { + headers.accept = EventStreamContentType; + } + + let curRequestController: AbortController; + + function dispose() { + curRequestController.abort(); + } + + async function create() { + curRequestController = new AbortController(); + try { + const response = await fetch(input, { + ...rest, + headers, + signal: curRequestController.signal, + }); + + await onOpen(response); + + await getBytes( + response.body!, + getLines( + getMessages( + (id) => { + if (id) { + // store the id and send it back on the next retry: + headers[LastEventId] = id; + } else { + // don't send the last-event-id header anymore: + delete headers[LastEventId]; + } + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (_retry) => {}, + onMessage, + ), + ), + ); + + dispose(); + resolve(); + } catch (err) { + reject(err); + } + } + + create(); + }); +} + +function onOpen(response: Response) { + const contentType = response.headers.get("content-type"); + if (!contentType?.startsWith(EventStreamContentType)) { + throw new Error( + `Expected content-type to be ${EventStreamContentType}, Actual: ${contentType}`, + ); + } +} diff --git a/vscode/src/parse.ts b/vscode/src/parse.ts new file mode 100644 index 0000000000..e8617559b2 --- /dev/null +++ b/vscode/src/parse.ts @@ -0,0 +1,193 @@ +/** + * Represents a message sent in an event stream + * https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format + */ +export interface EventSourceMessage { + /** The event ID to set the EventSource object's last event ID value. */ + id: string; + /** A string identifying the type of event described. */ + event: string; + /** The event data */ + data: string; + /** The reconnection interval (in milliseconds) to wait before retrying the connection */ + retry?: number; +} + +/** + * Converts a ReadableStream into a callback pattern. + * @param stream The input ReadableStream. + * @param onChunk A function that will be called on each new byte chunk in the stream. + * @returns {Promise} A promise that will be resolved when the stream closes. + */ +export async function getBytes( + stream: ReadableStream, + onChunk: (arr: Uint8Array) => void, +) { + const reader = stream.getReader(); + let result: ReadableStreamReadResult; + while (!(result = await reader.read()).done) { + onChunk(result.value); + } +} + +const enum ControlChars { + NewLine = 10, + CarriageReturn = 13, + Space = 32, + Colon = 58, +} + +/** + * Parses arbitary byte chunks into EventSource line buffers. + * Each line should be of the format "field: value" and ends with \r, \n, or \r\n. + * @param onLine A function that will be called on each new EventSource line. + * @returns A function that should be called for each incoming byte chunk. + */ +export function getLines( + onLine: (line: Uint8Array, fieldLength: number) => void, +) { + let buffer: Uint8Array | undefined; + let position: number; // current read position + let fieldLength: number; // length of the `field` portion of the line + let discardTrailingNewline = false; + + // return a function that can process each incoming byte chunk: + return function onChunk(arr: Uint8Array) { + if (buffer === undefined) { + buffer = arr; + position = 0; + fieldLength = -1; + } else { + // we're still parsing the old line. Append the new bytes into buffer: + buffer = concat(buffer, arr); + } + + const bufLength = buffer.length; + let lineStart = 0; // index where the current line starts + while (position < bufLength) { + if (discardTrailingNewline) { + if (buffer[position] === ControlChars.NewLine) { + lineStart = ++position; // skip to next char + } + + discardTrailingNewline = false; + } + + // start looking forward till the end of line: + let lineEnd = -1; // index of the \r or \n char + for (; position < bufLength && lineEnd === -1; ++position) { + switch (buffer[position]) { + case ControlChars.Colon: + if (fieldLength === -1) { + // first colon in line + fieldLength = position - lineStart; + } + break; + case ControlChars.CarriageReturn: + discardTrailingNewline = true; + lineEnd = position; + break; + case ControlChars.NewLine: + lineEnd = position; + break; + } + } + + if (lineEnd === -1) { + // We reached the end of the buffer but the line hasn't ended. + // Wait for the next arr and then continue parsing: + break; + } + + // we've reached the line end, send it out: + onLine(buffer.subarray(lineStart, lineEnd), fieldLength); + lineStart = position; // we're now on the next line + fieldLength = -1; + } + + if (lineStart === bufLength) { + buffer = undefined; // we've finished reading it + } else if (lineStart !== 0) { + // Create a new view into buffer beginning at lineStart so we don't + // need to copy over the previous lines when we get the new arr: + buffer = buffer.subarray(lineStart); + position -= lineStart; + } + }; +} + +/** + * Parses line buffers into EventSourceMessages. + * @param onId A function that will be called on each `id` field. + * @param onRetry A function that will be called on each `retry` field. + * @param onMessage A function that will be called on each message. + * @returns A function that should be called for each incoming line buffer. + */ +export function getMessages( + onId: (id: string) => void, + onRetry: (retry: number) => void, + onMessage?: (msg: EventSourceMessage) => void, +) { + let message = newMessage(); + const decoder = new TextDecoder(); + + // return a function that can process each incoming line buffer: + return function onLine(line: Uint8Array, fieldLength: number) { + if (line.length === 0) { + // empty line denotes end of message. Trigger the callback and start a new message: + onMessage?.(message); + message = newMessage(); + } else if (fieldLength > 0) { + // exclude comments and lines with no values + // line is of format ":" or ": " + // https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + const field = decoder.decode(line.subarray(0, fieldLength)); + const valueOffset = + fieldLength + (line[fieldLength + 1] === ControlChars.Space ? 2 : 1); + const value = decoder.decode(line.subarray(valueOffset)); + + switch (field) { + case "data": + // if this message already has data, append the new value to the old. + // otherwise, just set to the new value: + message.data = message.data ? message.data + "\n" + value : value; // otherwise, + break; + case "event": + message.event = value; + break; + case "id": + onId((message.id = value)); + break; + case "retry": + { + const retry = parseInt(value, 10); + if (!isNaN(retry)) { + // per spec, ignore non-integers + onRetry((message.retry = retry)); + } + } + break; + } + } + }; +} + +function concat(a: Uint8Array, b: Uint8Array) { + const res = new Uint8Array(a.length + b.length); + res.set(a); + res.set(b, a.length); + return res; +} + +function newMessage(): EventSourceMessage { + // data, event, and id must be initialized to empty strings: + // https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + // retry should be initialized to undefined so we return a consistent shape + // to the js engine all the time: https://mathiasbynens.be/notes/shapes-ics#takeaways + return { + data: "", + event: "", + id: "", + retry: undefined, + }; +} From 8ab8f3d8ef352d161ca654799317780a9ecc487c Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Mon, 22 Jul 2024 14:28:05 -0700 Subject: [PATCH 07/11] reorg --- vscode/src/{ => copilot}/chatParticipant.ts | 4 ++-- vscode/src/{ => copilot}/fetch.ts | 0 vscode/src/{ => copilot}/parse.ts | 0 vscode/src/extension.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename vscode/src/{ => copilot}/chatParticipant.ts (97%) rename vscode/src/{ => copilot}/fetch.ts (100%) rename vscode/src/{ => copilot}/parse.ts (100%) diff --git a/vscode/src/chatParticipant.ts b/vscode/src/copilot/chatParticipant.ts similarity index 97% rename from vscode/src/chatParticipant.ts rename to vscode/src/copilot/chatParticipant.ts index 621b128533..fa5d901056 100644 --- a/vscode/src/chatParticipant.ts +++ b/vscode/src/copilot/chatParticipant.ts @@ -2,9 +2,9 @@ // Licensed under the MIT License. import * as vscode from "vscode"; -import { getRandomGuid } from "./utils"; +import { getRandomGuid } from "../utils"; import { log } from "qsharp-lang"; -import { getAuthSession, scopes } from "./azure/auth"; +import { getAuthSession, scopes } from "../azure/auth"; import { fetchEventSource } from "./fetch"; const chatUrl = diff --git a/vscode/src/fetch.ts b/vscode/src/copilot/fetch.ts similarity index 100% rename from vscode/src/fetch.ts rename to vscode/src/copilot/fetch.ts diff --git a/vscode/src/parse.ts b/vscode/src/copilot/parse.ts similarity index 100% rename from vscode/src/parse.ts rename to vscode/src/copilot/parse.ts diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 811bbb63a2..03530e9ca5 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -59,7 +59,7 @@ import { sendTelemetryEvent, } from "./telemetry.js"; import { registerWebViewCommands } from "./webviewPanel.js"; -import { activateChatParticipant } from "./chatParticipant.js"; +import { activateChatParticipant } from "./copilot/chatParticipant.js"; export async function activate( context: vscode.ExtensionContext, From cef1faedba1384c0be0f80289d871ed86543444b Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Mon, 22 Jul 2024 15:39:38 -0700 Subject: [PATCH 08/11] added MIT license references to the top of the files --- vscode/src/copilot/fetch.ts | 3 +++ vscode/src/copilot/parse.ts | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/vscode/src/copilot/fetch.ts b/vscode/src/copilot/fetch.ts index 1886f721d9..9ed8955319 100644 --- a/vscode/src/copilot/fetch.ts +++ b/vscode/src/copilot/fetch.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { EventSourceMessage, getBytes, getLines, getMessages } from "./parse"; export const EventStreamContentType = "text/event-stream"; diff --git a/vscode/src/copilot/parse.ts b/vscode/src/copilot/parse.ts index e8617559b2..a662450462 100644 --- a/vscode/src/copilot/parse.ts +++ b/vscode/src/copilot/parse.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + /** * Represents a message sent in an event stream * https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format @@ -38,7 +41,7 @@ const enum ControlChars { } /** - * Parses arbitary byte chunks into EventSource line buffers. + * Parses arbitrary byte chunks into EventSource line buffers. * Each line should be of the format "field: value" and ends with \r, \n, or \r\n. * @param onLine A function that will be called on each new EventSource line. * @returns A function that should be called for each incoming byte chunk. From 38eaea188f0d8672ee185dc7e773d95b88c05321 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Tue, 23 Jul 2024 13:53:59 -0700 Subject: [PATCH 09/11] added fullname to the chat participant --- vscode/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/vscode/package.json b/vscode/package.json index 07abf2d36a..002128cc6d 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -481,6 +481,7 @@ { "id": "quantum.copilot", "name": "quantum", + "fullName": "Azure Quantum Copilot", "description": "Azure Quantum Copilot", "isSticky": true, "commands": [ From ed7e1487acf84b566f16c9054adb83595e88a65a Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Tue, 23 Jul 2024 16:42:17 -0700 Subject: [PATCH 10/11] update fullName to "Azure Quantum" --- vscode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode/package.json b/vscode/package.json index 002128cc6d..d66486d4c6 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -481,7 +481,7 @@ { "id": "quantum.copilot", "name": "quantum", - "fullName": "Azure Quantum Copilot", + "fullName": "Azure Quantum", "description": "Azure Quantum Copilot", "isSticky": true, "commands": [ From 246c6d4d55efba2435016e561493106a5acd3325 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 24 Jul 2024 11:16:17 -0700 Subject: [PATCH 11/11] update to use the production URL over the canary URL --- vscode/src/copilot/chatParticipant.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vscode/src/copilot/chatParticipant.ts b/vscode/src/copilot/chatParticipant.ts index fa5d901056..bb5d6d3aa9 100644 --- a/vscode/src/copilot/chatParticipant.ts +++ b/vscode/src/copilot/chatParticipant.ts @@ -7,8 +7,7 @@ import { log } from "qsharp-lang"; import { getAuthSession, scopes } from "../azure/auth"; import { fetchEventSource } from "./fetch"; -const chatUrl = - "https://westus3.aqa.canary.quantum.azure.com/api/chat/streaming"; +const chatUrl = "https://westus3.aqa.quantum.azure.com/api/chat/streaming"; const chatApp = "652066ed-7ea8-4625-a1e9-5bac6600bf06"; type quantumChatRequest = {