diff --git a/src/copilot-chat/copilotChat.ts b/src/copilot-chat/copilotChat.ts index c197b0f2..a120efc0 100644 --- a/src/copilot-chat/copilotChat.ts +++ b/src/copilot-chat/copilotChat.ts @@ -12,11 +12,11 @@ let specifications: ApiCenterApiVersionDefinitionExport[] = []; let promptFind = ''; export interface IChatAgentResult extends vscode.ChatAgentResult2 { - slashCommand: string; + subCommand: string; } export async function handleChatMessage(request: vscode.ChatAgentRequest, ctx: vscode.ChatAgentContext, progress: vscode.Progress, token: vscode.CancellationToken): Promise { - const cmd = request.slashCommand?.name; + const cmd = request.subCommand; const eventName = cmd ? `${TelemetryEvent.copilotChat}.${cmd}` : TelemetryEvent.copilotChat; let parsedError: IParsedError | undefined; @@ -27,7 +27,7 @@ export async function handleChatMessage(request: vscode.ChatAgentRequest, ctx: v if (!cmd) { progress.report({ content: 'Hi! What can I help you with? Please use `/list` or `/find` to chat with me!' }); - return { slashCommand: '' }; + return { subCommand: '' }; } if (['list', 'find'].includes(cmd ?? "")) { @@ -45,7 +45,7 @@ export async function handleChatMessage(request: vscode.ChatAgentRequest, ctx: v const specificationsToShow = specifications.slice(index, index + specificationsCount); if (specificationsToShow.length === 0) { progress.report({ content: "\`>\` There are no more API Specifications.\n\n" }); - return { slashCommand: '' }; + return { subCommand: '' }; } specificationsContent = specificationsToShow.map((specification, index) => `## Spec ${index + 1}:\n${specification.value}\n`).join('\n'); } @@ -69,7 +69,7 @@ export async function handleChatMessage(request: vscode.ChatAgentRequest, ctx: v const incomingText = fragment.replace('[RESPONSE END]', ''); progress.report({ content: incomingText }); } - return { slashCommand: 'list' }; + return { subCommand: 'list' }; } else if ((cmd === 'find')) { progress.report({ content: `\`>\` Parsing API Specifications for '${promptFind}'...\n\n` }); const access = await vscode.chat.requestChatAccess('copilot'); @@ -89,10 +89,10 @@ export async function handleChatMessage(request: vscode.ChatAgentRequest, ctx: v const incomingText = fragment.replace('[RESPONSE END]', ''); progress.report({ content: incomingText }); } - return { slashCommand: 'find' }; + return { subCommand: 'find' }; } - return { slashCommand: '' }; + return { subCommand: '' }; } catch (error) { parsedError = parseError(error); throw error; diff --git a/src/copilot-chat/typings/vscode.proposed.chatAgents2.d.ts b/src/copilot-chat/typings/vscode.proposed.chatAgents2.d.ts index 5454588d..e303c682 100644 --- a/src/copilot-chat/typings/vscode.proposed.chatAgents2.d.ts +++ b/src/copilot-chat/typings/vscode.proposed.chatAgents2.d.ts @@ -5,38 +5,90 @@ declare module 'vscode' { + export interface ChatAgentHistoryEntry { + request: ChatAgentRequest; + response: ChatAgentContentProgress[]; + result: ChatAgentResult2; + } + export interface ChatAgentContext { /** * All of the chat messages so far in the current chat session. */ - history: ChatMessage[]; + history: ChatAgentHistoryEntry[]; } + /** + * Represents an error result from a chat request. + */ export interface ChatAgentErrorDetails { + /** + * An error message that is shown to the user. + */ message: string; + + /** + * If partial markdown content was sent over the `progress` callback 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 ChatAgentResult2 { + /** + * If the request resulted in an error, this property defines the error details. + */ errorDetails?: ChatAgentErrorDetails; } + /** + * Represents the type of user feedback received. + */ export enum ChatAgentResultFeedbackKind { + /** + * The user marked the result as helpful. + */ Unhelpful = 0, + + /** + * The user marked the result as unhelpful. + */ Helpful = 1, } - export interface ChatAgentResult2Feedback { - readonly result: ChatAgentResult2; + /** + * Represents user feedback for a result. + */ + export interface ChatAgentResult2Feedback { + /** + * This instance of ChatAgentResult2 is the same instance that was returned from the chat agent, + * and it can be extended with arbitrary properties if needed. + */ + readonly result: TResult; + + /** + * The kind of feedback that was received. + */ readonly kind: ChatAgentResultFeedbackKind; } - export interface ChatAgentSlashCommand { - + export interface ChatAgentSubCommand { /** * A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. + * + * **Note**: The name should be unique among the subCommands provided by this agent. */ readonly name: string; @@ -44,24 +96,44 @@ declare module 'vscode' { * Human-readable description explaining what this command does. */ readonly description: string; + + /** + * When the user clicks this subCommand in `/help`, this text will be submitted to this subCommand + */ + readonly sampleRequest?: string; + + /** + * Whether executing the command puts the + * chat into a persistent mode, where the + * subCommand is prepended to the chat input. + */ + readonly shouldRepopulate?: boolean; + + /** + * Placeholder text to render in the chat input + * when the subCommand has been repopulated. + * Has no effect if `shouldRepopulate` is `false`. + */ + // TODO@API merge this with shouldRepopulate? so that invalid state cannot be represented? + readonly followupPlaceholder?: string; } - export interface ChatAgentSlashCommandProvider { + export interface ChatAgentSubCommandProvider { /** - * Returns a list of slash commands that its agent is capable of handling. A slash command - * and be selected by the user and will then be passed to the {@link ChatAgentHandler handler} - * via the {@link ChatAgentRequest.slashCommand slashCommand} property. + * Returns a list of subCommands that its agent is capable of handling. A subCommand + * can be selected by the user and will then be passed to the {@link ChatAgentHandler handler} + * via the {@link ChatAgentRequest.subCommand subCommand} property. * * * @param token A cancellation token. - * @returns A list of slash commands. The lack of a result can be signaled by returning `undefined`, `null`, or + * @returns A list of subCommands. The lack of a result can be signaled by returning `undefined`, `null`, or * an empty array. */ - provideSlashCommands(token: CancellationToken): ProviderResult; + provideSubCommands(token: CancellationToken): ProviderResult; } - // TODO@API is this just a vscode.Command? + // TODO@API This should become a progress type, and use vscode.Command // TODO@API what's the when-property for? how about not returning it in the first place? export interface ChatAgentCommandFollowup { commandId: string; @@ -70,27 +142,49 @@ declare module 'vscode' { when?: string; } + /** + * A followup question suggested by the model. + */ export interface ChatAgentReplyFollowup { + /** + * The message to send to the chat. + */ message: string; + + /** + * A tooltip to show when hovering over the followup. + */ tooltip?: string; + + /** + * A title to show the user, when it is different than the message. + */ title?: string; } export type ChatAgentFollowup = ChatAgentCommandFollowup | ChatAgentReplyFollowup; - export interface FollowupProvider { - provideFollowups(result: ChatAgentResult2, token: CancellationToken): ProviderResult; + /** + * 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 FollowupProvider { + /** + * + * @param result The same instance of the result object that was returned by the chat agent, and it can be extended with arbitrary properties if needed. + * @param token A cancellation token. + */ + provideFollowups(result: TResult, token: CancellationToken): ProviderResult; } - export interface ChatAgent2 { + export interface ChatAgent2 { /** - * The short name by which this agent is referred to in the UI, e.g `workspace` + * The short name by which this agent is referred to in the UI, e.g `workspace`. */ readonly name: string; /** - * The full name of this agent + * The full name of this agent. */ fullName: string; @@ -102,11 +196,31 @@ declare module 'vscode' { /** * Icon for the agent shown in UI. */ - iconPath?: Uri; + iconPath?: Uri | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } | ThemeIcon; + + /** + * This provider will be called to retrieve the agent's subCommands. + */ + subCommandProvider?: ChatAgentSubCommandProvider; - slashCommandProvider?: ChatAgentSlashCommandProvider; + /** + * This provider will be called once after each request to retrieve suggested followup questions. + */ + followupProvider?: FollowupProvider; - followupProvider?: FollowupProvider; + /** + * When the user clicks this agent in `/help`, this text will be submitted to this subCommand + */ + sampleRequest?: string; /** * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes @@ -115,13 +229,9 @@ declare module 'vscode' { * The passed {@link ChatAgentResult2Feedback.result result} is guaranteed to be the same instance that was * previously returned from this chat agent. */ - onDidReceiveFeedback: Event; - - // TODO@API Something like prepareSession from the interactive chat provider might be needed.Probably nobody needs it right now. - // prepareSession(); + onDidReceiveFeedback: Event>; /** - * TODO@API explain what happens wrt to history, in-flight requests etc... * Dispose this agent and free resources */ dispose(): void; @@ -130,30 +240,51 @@ declare module 'vscode' { export interface ChatAgentRequest { /** - * The prompt entered by the user. The {@link ChatAgent2.name name} of the agent or the {@link ChatAgentSlashCommand.name slash command} + * The prompt entered by the user. The {@link ChatAgent2.name name} of the agent or the {@link ChatAgentSubCommand.name subCommand} * are not part of the prompt. * - * @see {@link ChatAgentRequest.slashCommand} + * @see {@link ChatAgentRequest.subCommand} */ prompt: string; /** - * The {@link ChatAgentSlashCommand slash command} that was selected for this request. It is guaranteed that the passed slash - * command is an instance that was previously returned from the {@link ChatAgentSlashCommandProvider.provideSlashCommands slash command provider}. + * The ID of the chat agent to which this request was directed. */ - slashCommand?: ChatAgentSlashCommand; + agentId: string; + + /** + * The {@link ChatAgentSubCommand subCommand} that was selected for this request. It is guaranteed that the passed subCommand + * is an instance that was previously returned from the {@link ChatAgentSubCommandProvider.provideSubCommands subCommand provider}. + * @deprecated this will be replaced by `subCommand` + */ + slashCommand?: ChatAgentSubCommand; + + /** + * The name of the {@link ChatAgentSubCommand subCommand} that was selected for this request. + */ + subCommand?: string; variables: Record; } - // TODO@API should these each be prefixed ChatAgentProgress*? - export type ChatAgentProgress = + export type ChatAgentContentProgress = | ChatAgentContent - | ChatAgentTask | ChatAgentFileTree + | ChatAgentInlineContentReference; + + export type ChatAgentMetadataProgress = | ChatAgentUsedContext | ChatAgentContentReference - | ChatAgentInlineContentReference; + | ChatAgentProgressMessage; + + export type ChatAgentProgress = ChatAgentContentProgress | ChatAgentMetadataProgress; + + /** + * Is displayed in the UI to communicate steps of progress to the user. Should be used when the agent may be slow to respond, e.g. due to doing extra work before sending the actual request to the LLM. + */ + export interface ChatAgentProgressMessage { + message: string; + } /** * Indicates a piece of content that was used by the chat agent while processing the request. Will be displayed to the user. @@ -190,22 +321,6 @@ declare module 'vscode' { content: string; } - /** - * Represents a piece of the chat response's content that is resolved asynchronously. It is rendered immediately with a placeholder, - * which is replaced once the full content is available. - */ - export interface ChatAgentTask { - /** - * The markdown string to be rendered immediately. - */ - placeholder: string; - - /** - * A Thenable resolving to the real content. The placeholder will be replaced with this content once it's available. - */ - resolvedContent: Thenable; - } - /** * Represents a tree, such as a file and directory structure, rendered in the chat response. */ @@ -228,8 +343,17 @@ declare module 'vscode' { /** * A Uri for this node, opened when it's clicked. */ + // TODO@API why label and uri. Can the former be derived from the latter? + // TODO@API don't use uri but just names? This API allows to to build nonsense trees where the data structure doesn't match the uris + // path-structure. uri: Uri; + /** + * The type of this node. Defaults to {@link FileType.Directory} if it has {@link ChatAgentFileTreeData.children children}. + */ + // TODO@API cross API usage + type?: FileType; + /** * The children of this node. */ @@ -260,6 +384,57 @@ declare module 'vscode' { * @param handler The reply-handler of the agent. * @returns A new chat agent */ - export function createChatAgent(name: string, handler: ChatAgentHandler): ChatAgent2; + export function createChatAgent(name: string, handler: ChatAgentHandler): ChatAgent2; + + /** + * Register a variable which can be used in a chat request to any agent. + * @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 registerVariable(name: string, description: string, resolver: ChatVariableResolver): Disposable; + } + + /** + * 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 agent 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; + } + + export interface ChatVariableContext { + /** + * The message entered by the user, which includes this variable. + */ + prompt: string; + } + + 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/src/copilot-chat/typings/vscode.proposed.chatRequestAccess.d.ts b/src/copilot-chat/typings/vscode.proposed.chatRequestAccess.d.ts index 230366ae..f696289c 100644 --- a/src/copilot-chat/typings/vscode.proposed.chatRequestAccess.d.ts +++ b/src/copilot-chat/typings/vscode.proposed.chatRequestAccess.d.ts @@ -67,6 +67,12 @@ declare module 'vscode' { */ isRevoked: boolean; + /** + * The name of the model that is used for this chat access. It is expected that the model name can + * be used to lookup properties like token limits and ChatML support + */ + model: string; + /** * Make a chat request. * diff --git a/src/copilot-chat/typings/vscode.proposed.chatVariables.d.ts b/src/copilot-chat/typings/vscode.proposed.chatVariables.d.ts deleted file mode 100644 index 6d9de44b..00000000 --- a/src/copilot-chat/typings/vscode.proposed.chatVariables.d.ts +++ /dev/null @@ -1,35 +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 interface InteractiveRequest { - variables: Record; - } - - export enum ChatVariableLevel { - Short = 1, - Medium = 2, - Full = 3 - } - - export interface ChatVariableValue { - level: ChatVariableLevel; - value: string; - description?: string; - } - - export interface ChatVariableContext { - message: string; - } - - export interface ChatVariableResolver { - resolve(name: string, context: ChatVariableContext, token: CancellationToken): ProviderResult; - } - - export namespace chat { - export function registerVariable(name: string, description: string, resolver: ChatVariableResolver): Disposable; - } -} diff --git a/src/extension.ts b/src/extension.ts index 03366771..fa112d56 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -90,8 +90,8 @@ export async function activate(context: vscode.ExtensionContext) { const agent = vscode.chat.createChatAgent('apicenter', handleChatMessage); agent.description = 'Build, discover, and consume great APIs.'; agent.fullName = "Azure API Center"; - agent.slashCommandProvider = { - provideSlashCommands(token) { + agent.subCommandProvider = { + provideSubCommands(token) { return [ { name: 'list', @@ -106,12 +106,12 @@ export async function activate(context: vscode.ExtensionContext) { }; agent.followupProvider = { provideFollowups(result: IChatAgentResult, token: vscode.CancellationToken) { - if (result.slashCommand === 'list') { + if (result.subCommand === 'list') { return [{ message: '@apicenter /list $more', title: 'List more APIs' }]; - } else if (result.slashCommand === 'find') { + } else if (result.subCommand === 'find') { return [{ message: '@apicenter /find $more', title: 'Find in more APIs'