Add chat-agent sample

This commit is contained in:
isidor
2023-11-13 16:30:27 +01:00
parent 4721ef0c45
commit 183f941e7b
17 changed files with 2508 additions and 0 deletions

View File

@ -0,0 +1,95 @@
import * as vscode from 'vscode';
const CAT_LEARN_SYSTEM_PROMPT = 'You are a cat! Your job is to explain computer science concepts in a funny manner of a cat. Always start your response by stating what concept you are explaining.';
const CAT_PLAY_SYSTEM_PROMPT = '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.';
const MEOW_COMMAND_ID = 'cat.meow';
export function activate(context: vscode.ExtensionContext) {
const teachResult = { /* you can return anything in your result object */ };
const playResult = { /* you can return anything in your result object */ };
// Define a Cat chat agent handler.
const handler: vscode.ChatAgentHandler = async (request: vscode.ChatAgentRequest, context: vscode.ChatAgentContext, progress: vscode.Progress<vscode.ChatAgentProgress>, token: vscode.CancellationToken) => {
// To talk to an LLM in your slash command handler implementation, your
// extension can use VS Code's `requestChatAccess` API to access the Copilot API.
// The pre-release of the GitHub Copilot Chat extension implements this provider.
if (request.slashCommand?.name == 'teach') {
const access = await vscode.chat.requestChatAccess('copilot');
const topics = ["linked list", "recursion", "stack", "queue", "pointers"];
const topic = topics[Math.floor(Math.random() * topics.length)];
const messages = [
{
role: vscode.ChatMessageRole.System,
content: CAT_LEARN_SYSTEM_PROMPT
},
{
role: vscode.ChatMessageRole.User,
content: topic
},
];
const request = access.makeRequest(messages, {}, token);
for await (const fragment of request.response) {
const incomingText = fragment.replace('[RESPONSE END]', '');
progress.report({ content: incomingText });
}
return teachResult;
} else if (request.slashCommand?.name == 'play') {
const access = await vscode.chat.requestChatAccess('copilot');
const messages = [
{
role: vscode.ChatMessageRole.System,
content: CAT_PLAY_SYSTEM_PROMPT
},
];
const request = access.makeRequest(messages, {}, token);
for await (const fragment of request.response) {
const incomingText = fragment.replace('[RESPONSE END]', '');
progress.report({ content: incomingText });
}
return playResult;
}
};
// Agents appear as top-level options in the chat input
// when you type `@`, and can contribute sub-commands in the chat input
// that appear when you type `/`.
const agent = vscode.chat.createChatAgent('cat', handler);
agent.iconPath = vscode.Uri.joinPath(context.extensionUri, 'cat.jpeg');
agent.description = vscode.l10n.t('Meow! What can I help you with?');
agent.fullName = vscode.l10n.t('Cat');
agent.slashCommandProvider = {
provideSlashCommands(token) {
return [
{ name: 'teach', description: 'Pick at random a computer science concept then explain it in purfect way of a cat' },
{ name: 'play', description: 'Do whatever you want, you are a cat after all' }
];
}
};
agent.followupProvider = {
provideFollowups(result, token) {
if (result === teachResult) {
return [{
commandId: MEOW_COMMAND_ID,
message: '@cat thank you',
title: vscode.l10n.t('Meow!')
}];
} else if (result === playResult) {
return [{
message: '@cat let us play',
title: vscode.l10n.t('Play with the cat')
}];
}
}
};
context.subscriptions.push(
agent,
// Register the command handler for the /meow followup
vscode.commands.registerCommand(MEOW_COMMAND_ID, async () => {
vscode.window.showInformationMessage('Meow!');
}),
);
}
export function deactivate() { }

View File

@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* 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);
}
}

View File

@ -0,0 +1,368 @@
/*---------------------------------------------------------------------------------------------
* 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 ChatAgentContext {
/**
* All of the chat messages so far in the current chat session.
*/
history: ChatMessage[];
}
/**
* 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,
}
/**
* 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: ChatAgentResult2;
/**
* The kind of feedback that was received.
*/
readonly kind: ChatAgentResultFeedbackKind;
}
export interface ChatAgentSlashCommand {
/**
* 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 slash commands provided by this agent.
*/
readonly name: string;
/**
* Human-readable description explaining what this command does.
*/
readonly description: string;
/**
* When the user clicks this slash command in `/help`, this text will be submitted to this slash command
*/
readonly sampleRequest?: string;
/**
* Whether executing the command puts the
* chat into a persistent mode, where the
* slash command is prepended to the chat input.
*/
readonly shouldRepopulate?: boolean;
/**
* Placeholder text to render in the chat input
* when the slash command has been repopulated.
* Has no effect if `shouldRepopulate` is `false`.
*/
readonly followupPlaceholder?: string;
}
export interface ChatAgentSlashCommandProvider {
/**
* Returns a list of slash commands that its agent is capable of handling. A slash command
* can be selected by the user and will then be passed to the {@link ChatAgentHandler handler}
* via the {@link ChatAgentRequest.slashCommand slashCommand} 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
* an empty array.
*/
provideSlashCommands(token: CancellationToken): ProviderResult<ChatAgentSlashCommand[]>;
}
// 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;
args?: any[];
title: string; // supports codicon strings
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;
/**
* 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: ChatAgentResult2, token: CancellationToken): ProviderResult<ChatAgentFollowup[]>;
}
export interface ChatAgent2 {
/**
* 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.
*/
fullName: string;
/**
* A human-readable description explaining what this agent does.
*/
description: string;
/**
* Icon for the agent shown in UI.
*/
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 slash commands.
*/
slashCommandProvider?: ChatAgentSlashCommandProvider;
/**
* This provider will be called once after each request to retrieve suggested followup questions.
*/
followupProvider?: FollowupProvider;
/**
* When the user clicks this agent in `/help`, this text will be submitted to this slash command
*/
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 ChatAgentResult2Feedback.result result} is guaranteed to be the same instance that was
* previously returned from this chat agent.
*/
onDidReceiveFeedback: Event<ChatAgentResult2Feedback>;
/**
* TODO@API explain what happens wrt to history, in-flight requests etc...
* Dispose this agent and free resources
*/
dispose(): void;
}
export interface ChatAgentRequest {
/**
* The prompt entered by the user. The {@link ChatAgent2.name name} of the agent or the {@link ChatAgentSlashCommand.name slash command}
* are not part of the prompt.
*
* @see {@link ChatAgentRequest.slashCommand}
*/
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}.
*/
slashCommand?: ChatAgentSlashCommand;
variables: Record<string, ChatVariableValue[]>;
}
// TODO@API should these each be prefixed ChatAgentProgress*?
export type ChatAgentProgress =
| ChatAgentContent
| ChatAgentTask
| ChatAgentFileTree
| ChatAgentUsedContext
| ChatAgentContentReference
| ChatAgentInlineContentReference;
/**
* Indicates a piece of content that was used by the chat agent while processing the request. Will be displayed to the user.
*/
export interface ChatAgentContentReference {
/**
* The resource that was referenced.
*/
reference: Uri | Location;
}
/**
* A reference to a piece of content that will be rendered inline with the markdown content.
*/
export interface ChatAgentInlineContentReference {
/**
* The resource being referenced.
*/
inlineReference: Uri | Location;
/**
* An alternate title for the resource.
*/
title?: string;
}
/**
* A piece of the chat response's content. Will be merged with other progress pieces as needed, and rendered as markdown.
*/
export interface ChatAgentContent {
/**
* The content as a string of markdown source.
*/
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<ChatAgentContent | ChatAgentFileTree>;
}
/**
* Represents a tree, such as a file and directory structure, rendered in the chat response.
*/
export interface ChatAgentFileTree {
/**
* The root node of the tree.
*/
treeData: ChatAgentFileTreeData;
}
/**
* Represents a node in a chat response tree.
*/
export interface ChatAgentFileTreeData {
/**
* A human-readable string describing this node.
*/
label: string;
/**
* A Uri for this node, opened when it's clicked.
*/
uri: Uri;
/**
* The children of this node.
*/
children?: ChatAgentFileTreeData[];
}
export interface ChatAgentDocumentContext {
uri: Uri;
version: number;
ranges: Range[];
}
/**
* Document references that should be used by the MappedEditsProvider.
*/
export interface ChatAgentUsedContext {
documents: ChatAgentDocumentContext[];
}
export type ChatAgentHandler = (request: ChatAgentRequest, context: ChatAgentContext, progress: Progress<ChatAgentProgress>, token: CancellationToken) => ProviderResult<ChatAgentResult2>;
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.
* @returns A new chat agent
*/
export function createChatAgent(name: string, handler: ChatAgentHandler): ChatAgent2;
}
}

View File

@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* 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 ChatResponseStream {
/**
* The response stream.
*/
readonly response: AsyncIterable<string>;
/**
* The variant of multiple responses. This is used to disambiguate between multiple
* response streams when having asked for multiple response options
*/
readonly option: number;
}
export interface ChatRequest {
/**
* The overall result of the request which represents failure or success
* but _not_ the actual response or responses
*/
result: Thenable<any>;
/**
* The _default response_ stream. This is the stream of the first response option
* receiving data.
*
* Usually there is only one response option and this stream is more convienient to use
* than the {@link onDidStartResponseStream `onDidStartResponseStream`} event.
*/
response: AsyncIterable<string>;
/**
* An event that fires whenever a new response option is available. The response
* itself is a stream of the actual response.
*
* *Note* that the first time this event fires, the {@link ChatResponseStream.response response stream}
* is the same as the {@link response `default response stream`}.
*
* *Note* that unless requested there is only one response option, so this event will only fire
* once.
*/
onDidStartResponseStream: Event<ChatResponseStream>;
/**
* Cancel this request.
*/
// TODO@API remove this? We pass a token to makeRequest call already
cancel(): void;
}
/**
* Represents access to using a chat provider (LLM). Access is granted and temporary, usually only valid
* for the duration of an user interaction or specific time frame.
*/
export interface ChatAccess {
/**
* Whether the access to chat has been revoked. This happens when the condition that allowed for
* chat access doesn't hold anymore, e.g a user interaction has ended.
*/
isRevoked: boolean;
/**
* Make a chat request.
*
* The actual response will be reported back via the `progress` callback. The promise returned by this function
* returns a overall result which represents failure or success of the request.
*
* Chat can be asked for multiple response options. In that case the `progress` callback will be called multiple
* time with different `ChatResponseStream` objects. Each object represents a different response option and the actual
* response will be reported back via their `stream` property.
*
* *Note:* This will throw an error if access has been revoked.
*
* @param messages
* @param options
*/
makeRequest(messages: ChatMessage[], options: { [name: string]: any }, token: CancellationToken): ChatRequest;
}
export namespace chat {
/**
* Request access to chat.
*
* *Note* that this function will throw an error unless an user interaction is currently active.
*
* @param id The id of the chat provider, e.g `copilot`
*/
export function requestChatAccess(id: string): Thenable<ChatAccess>;
}
}