diff --git a/chat-agent-sample/src/extension.ts b/chat-agent-sample/src/extension.ts index 9e0dd870..e7f59b0b 100644 --- a/chat-agent-sample/src/extension.ts +++ b/chat-agent-sample/src/extension.ts @@ -13,7 +13,7 @@ const LANGUAGE_MODEL_ID = 'copilot-gpt-4'; export function activate(context: vscode.ExtensionContext) { // Define a Cat chat agent handler. - const handler: vscode.ChatAgentHandler = async (request: vscode.ChatAgentRequest, context: vscode.ChatAgentContext, stream: vscode.ChatAgentResponseStream, token: vscode.CancellationToken): Promise => { + const handler: vscode.ChatAgentRequestHandler = async (request: vscode.ChatAgentRequest, context: vscode.ChatAgentContext, stream: vscode.ChatAgentResponseStream, token: vscode.CancellationToken): Promise => { // To talk to an LLM in your subcommand handler implementation, your // extension can use VS Code's `requestChatAccess` API to access the Copilot API. // The GitHub Copilot Chat extension implements this provider. @@ -22,14 +22,8 @@ export function activate(context: vscode.ExtensionContext) { const topics = ['linked list', 'recursion', 'stack', 'queue', 'pointers']; const topic = topics[Math.floor(Math.random() * topics.length)]; const messages = [ - { - role: vscode.ChatMessageRole.System, - content: 'You are a cat! Your job is to explain computer science concepts in the funny manner of a cat. Always start your response by stating what concept you are explaining.' - }, - { - role: vscode.ChatMessageRole.User, - content: topic - }, + new vscode.LanguageModelSystemMessage('You are a cat! Your job is to explain computer science concepts in the funny manner of a cat. Always start your response by stating what concept you are explaining.'), + new vscode.LanguageModelUserMessage(topic) ]; const chatRequest = access.makeChatRequest(messages, {}, token); for await (const fragment of chatRequest.stream) { @@ -45,14 +39,8 @@ export function activate(context: vscode.ExtensionContext) { } else if (request.command == 'play') { const access = await vscode.chat.requestLanguageModelAccess(LANGUAGE_MODEL_ID); const messages = [ - { - role: vscode.ChatMessageRole.System, - content: 'You are a cat that wants to play! Reply in a helpful way for a coder, but with the hidden meaning that all you want to do is play.' - }, - { - role: vscode.ChatMessageRole.User, - content: request.prompt - } + new vscode.LanguageModelSystemMessage('You are a cat that wants to play! Reply in a helpful way for a coder, but with the hidden meaning that all you want to do is play.'), + new vscode.LanguageModelUserMessage(request.prompt) ]; const chatRequest = access.makeChatRequest(messages, {}, token); for await (const fragment of chatRequest.stream) { @@ -62,14 +50,8 @@ export function activate(context: vscode.ExtensionContext) { } else { const access = await vscode.chat.requestLanguageModelAccess(LANGUAGE_MODEL_ID); const messages = [ - { - role: vscode.ChatMessageRole.System, - content: 'You are a cat! Reply in the voice of a cat, using cat analogies when appropriate.' - }, - { - role: vscode.ChatMessageRole.User, - content: request.prompt - } + new vscode.LanguageModelSystemMessage('You are a cat! Reply in the voice of a cat, using cat analogies when appropriate.'), + new vscode.LanguageModelUserMessage(request.prompt) ]; const chatRequest = access.makeChatRequest(messages, {}, token); for await (const fragment of chatRequest.stream) { diff --git a/chat-agent-sample/src/vscode.proposed.chat.d.ts b/chat-agent-sample/src/vscode.proposed.chat.d.ts deleted file mode 100644 index 8585cb31..00000000 --- a/chat-agent-sample/src/vscode.proposed.chat.d.ts +++ /dev/null @@ -1,25 +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' { - - // ChatML - export enum ChatMessageRole { - System = 0, - User = 1, - Assistant = 2, - Function = 3, - } - - // ChatML - export class ChatMessage { - role: ChatMessageRole; - content: string; - name?: string; - - constructor(role: ChatMessageRole, content: string); - } - -} diff --git a/chat-agent-sample/src/vscode.proposed.chatAgents2.d.ts b/chat-agent-sample/src/vscode.proposed.chatAgents2.d.ts index 4ec0f8a9..82cdc761 100644 --- a/chat-agent-sample/src/vscode.proposed.chatAgents2.d.ts +++ b/chat-agent-sample/src/vscode.proposed.chatAgents2.d.ts @@ -5,33 +5,13 @@ declare module 'vscode' { - /** - * One request/response pair in chat history. - */ - export interface ChatAgentHistoryEntry { - /** - * The request that was sent to the chat agent. - */ - request: ChatAgentRequest; - - /** - * The content that was received from the chat agent. Only the progress parts that represent actual content (not metadata) are represented. - */ - response: ReadonlyArray; - - /** - * The result that was received from the chat agent. - */ - result: ChatAgentResult2; - } - // TODO@API name: Turn? export class ChatAgentRequestTurn { /** * The prompt as entered by the user. * - * Information about variables used in this request are is stored in {@link ChatAgentRequest.variables2}. + * Information about variables used in this request are is stored in {@link ChatAgentRequest.variables}. * * *Note* that the {@link ChatAgent2.name name} of the agent and the {@link ChatAgentCommand.name command} * are not part of the prompt. @@ -41,7 +21,8 @@ declare module 'vscode' { /** * The ID of the chat agent to which this request was directed. */ - readonly agentId: string; + // TODO@API NAME: agentId shouldbe agentName or just agent (because it is ChatAgent#name) + readonly agent: { readonly extensionId: string; readonly agent: string; readonly agentId: string }; /** * The name of the {@link ChatAgentCommand command} that was selected for this request. @@ -49,12 +30,11 @@ declare module 'vscode' { readonly command: string | undefined; /** - * + * The variables that were referenced in this message. */ - // TODO@API is this needed? readonly variables: ChatAgentResolvedVariable[]; - private constructor(prompt: string, agentId: string, command: string | undefined, variables: ChatAgentResolvedVariable[],); + private constructor(prompt: string, command: string | undefined, variables: ChatAgentResolvedVariable[], agent: { extensionId: string; agentId: string }); } // TODO@API name: Turn? @@ -70,24 +50,17 @@ declare module 'vscode' { */ readonly result: ChatAgentResult2; - readonly agentId: string; + // TODO@API NAME: agentId shouldbe agentName or just agent (because it is ChatAgent#name) + readonly agent: { readonly extensionId: string; readonly agent: string; readonly agentId: string }; - private constructor(response: ReadonlyArray, result: ChatAgentResult2, agentId: string); + private constructor(response: ReadonlyArray, result: ChatAgentResult2, agentId: { extensionId: string; agentId: string }); } export interface ChatAgentContext { - /** - * @deprecated - */ - history: ChatAgentHistoryEntry[]; - - // location: - /** * All of the chat messages so far in the current chat session. */ - // TODO@API name: histroy - readonly history2: ReadonlyArray; + readonly history: ReadonlyArray; } /** @@ -180,26 +153,17 @@ declare module 'vscode' { readonly sampleRequest?: string; /** - * Whether executing the command puts the - * chat into a persistent mode, where the - * command is prepended to the chat input. + * Whether executing the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message. + * If this is not set, the chat input will fall back to the agent after submitting this command. */ - readonly shouldRepopulate?: boolean; - - /** - * Placeholder text to render in the chat input - * when the command 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; + readonly isSticky?: boolean; } export interface ChatAgentCommandProvider { /** * Returns a list of commands that its agent is capable of handling. A command - * can be selected by the user and will then be passed to the {@link ChatAgentHandler handler} + * can be selected by the user and will then be passed to the {@link ChatAgentRequestHandler handler} * via the {@link ChatAgentRequest.command command} property. * * @@ -207,6 +171,7 @@ declare module 'vscode' { * @returns A list of commands. The lack of a result can be signaled by returning `undefined`, `null`, or * an empty array. */ + // TODO@API Q: should we provide the current history or last results for extra context? provideCommands(token: CancellationToken): ProviderResult; } @@ -216,16 +181,19 @@ declare module 'vscode' { export interface ChatAgentFollowup { /** * The message to send to the chat. - * TODO@API is it ok for variables to resolved from the text of this prompt, using the `#` syntax? */ prompt: string; /** - * By default, the followup goes to the same agent/subCommand. But these properties can be set to override that. + * By default, the followup goes to the same agent/command. But this property can be set to invoke a different agent. + * TODO@API do extensions need to specify the extensionID of the agent here as well? */ agentId?: string; - subCommand?: string; + /** + * By default, the followup goes to the same agent/command. But this property can be set to invoke a different command. + */ + command?: string; /** * A tooltip to show when hovering over the followup. @@ -235,6 +203,7 @@ declare module 'vscode' { /** * A title to show the user, when it is different than the message. */ + // TODO@API title vs tooltip? title?: string; } @@ -250,6 +219,12 @@ declare module 'vscode' { provideFollowups(result: ChatAgentResult2, token: CancellationToken): ProviderResult; } + /** + * A chat request handler is a callback that will be invoked when a request is made to a chat agent. + */ + export type ChatAgentRequestHandler = (request: ChatAgentRequest, context: ChatAgentContext, response: ChatAgentResponseStream, token: CancellationToken) => ProviderResult; + + export interface ChatAgent2 { /** @@ -281,6 +256,11 @@ declare module 'vscode' { dark: Uri; } | ThemeIcon; + /** + * The handler for requests to this agent. + */ + requestHandler: ChatAgentRequestHandler; + /** * This provider will be called to retrieve the agent's commands. */ @@ -291,16 +271,6 @@ declare module 'vscode' { */ followupProvider?: ChatAgentFollowupProvider; - - // TODO@API - // notify(request: ChatResponsePart, reference: string): boolean; - // BETTER - // requestResponseStream(callback: (stream: ChatAgentResponseStream) => void, why?: string): void; - - // TODO@API - // clear NEVER happens - // onDidClearResult(value: TResult): void; - /** * When the user clicks this agent in `/help`, this text will be submitted to this command */ @@ -321,49 +291,59 @@ declare module 'vscode' { 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 ChatAgentResolvedVariable { - name: string; - range: [start: number, end: number]; - values: ChatVariableValue[]; + + /** + * 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 ChatAgentRequest.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?` + readonly values: ChatVariableValue[]; } export interface ChatAgentRequest { - /** - * The prompt entered by the user. The {@link ChatAgent2.name name} of the agent or the {@link ChatAgentCommand.name command} - * are not part of the prompt. - * - * @see {@link ChatAgentRequest.command} - */ - prompt: string; - /** * The prompt as entered by the user. * - * Information about variables used in this request are is stored in {@link ChatAgentRequest.variables2}. + * Information about variables used in this request are is stored in {@link ChatAgentRequest.variables}. * * *Note* that the {@link ChatAgent2.name name} of the agent and the {@link ChatAgentCommand.name command} * are not part of the prompt. */ - prompt2: string; - - /** - * The ID of the chat agent to which this request was directed. - */ - agentId: string; + readonly prompt: string; /** * The name of the {@link ChatAgentCommand command} that was selected for this request. */ - command?: string; - - /** @deprecated */ - variables: Record; + 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 agent + * to further modify the prompt, for instance by inlining variable values or creating links to + * headings which contain the resolved values. vvariables 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. */ - variables2: ChatAgentResolvedVariable[]; + // TODO@API Q? are there implicit variables that are not part of the prompt? + readonly variables: readonly ChatAgentResolvedVariable[]; } export interface ChatAgentResponseStream { @@ -376,6 +356,7 @@ declare module 'vscode' { * @param value A plain text value. * @returns This stream. */ + // TODO@API remove? text(value: string): ChatAgentResponseStream; /** @@ -386,6 +367,7 @@ declare module 'vscode' { * @param value A markdown string or a string that should be interpreted as markdown. * @returns This stream. */ + // TODO@API NAME: content markdown(value: string | MarkdownString): ChatAgentResponseStream; /** @@ -450,11 +432,6 @@ declare module 'vscode' { push(part: ChatResponsePart): ChatAgentResponseStream; } - // TODO@API - // support ChatResponseCommandPart - // support ChatResponseTextEditPart - // support ChatResponseCodeReferencePart - // TODO@API should the name suffix differentiate between rendered items (XYZPart) // and metadata like XYZItem export class ChatResponseTextPart { @@ -486,6 +463,7 @@ declare module 'vscode' { export class ChatResponseProgressPart { value: string; + // TODO@API inline constructor(value: string); } @@ -499,24 +477,24 @@ declare module 'vscode' { constructor(value: Command); } + /** + * Represents the different chat response types. + */ export type ChatResponsePart = ChatResponseTextPart | ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; - // TODO@API Remove a different type of `request` so that they can - // evolve at their own pace - export type ChatAgentHandler = (request: ChatAgentRequest, context: ChatAgentContext, response: ChatAgentResponseStream, token: CancellationToken) => ProviderResult; - export namespace chat { /** * Create a new {@link ChatAgent2 chat agent} instance. * - * @param name Short name by which this agent is referred to in the UI - * @param handler The reply-handler of the agent. + * @param name Short name by which the agent is referred to in the UI. The name must be unique for the extension + * contributing the agent but can collide with names from other extensions. + * @param handler A request handler for the agent. * @returns A new chat agent */ - export function createChatAgent(name: string, handler: ChatAgentHandler): ChatAgent2; + export function createChatAgent(name: string, handler: ChatAgentRequestHandler): ChatAgent2; /** * Register a variable which can be used in a chat request to any agent. diff --git a/chat-agent-sample/src/vscode.proposed.chatRequestAccess.d.ts b/chat-agent-sample/src/vscode.proposed.chatRequestAccess.d.ts index e1a151e7..633c28dc 100644 --- a/chat-agent-sample/src/vscode.proposed.chatRequestAccess.d.ts +++ b/chat-agent-sample/src/vscode.proposed.chatRequestAccess.d.ts @@ -5,18 +5,96 @@ declare module 'vscode' { + /** + * Represents a language model response. + * + * @see {@link LanguageModelAccess.makeChatRequest} + */ export interface LanguageModelResponse { /** * The overall result of the request which represents failure or success - * but _not_ the actual response or responses + * but. The concrete value is not specified and depends on the selected language model. + * + * *Note* that the actual response represented by the {@link LanguageModelResponse.stream `stream`}-property */ - // TODO@API define this type! result: Thenable; + /** + * An async iterable that is a stream of text chunks forming the overall response. + */ 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 LanguageModelSystemMessage { + + /** + * 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 LanguageModelUserMessage { + + /** + * 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 LanguageModelAssistantMessage { + + /** + * The content of this message. + */ + content: string; + + /** + * Create a new assistant message. + * + * @param content The content of the message. + */ + constructor(content: string); + } + + export type LanguageModelMessage = LanguageModelSystemMessage | LanguageModelUserMessage | LanguageModelAssistantMessage; + /** * Represents access to using a language model. Access can be revoked at any time and extension * must check if the access is {@link LanguageModelAccess.isRevoked still valid} before using it. @@ -31,6 +109,7 @@ declare module 'vscode' { /** * An event that is fired when the access the language model has has been revoked or re-granted. */ + // TODO@API NAME? readonly onDidChangeAccess: Event; /** @@ -49,7 +128,7 @@ declare module 'vscode' { * @param messages * @param options */ - makeChatRequest(messages: ChatMessage[], options: { [name: string]: any }, token: CancellationToken): LanguageModelResponse; + makeChatRequest(messages: LanguageModelMessage[], options: { [name: string]: any }, token: CancellationToken): LanguageModelResponse; } export interface LanguageModelAccessOptions { @@ -73,7 +152,7 @@ declare module 'vscode' { readonly removed: readonly string[]; } - //@API DEFINE the namespace for this: env, lm, ai? + //@API DEFINE the namespace for this: lm (languageModels), copilot, ai, env,? export namespace chat { /**