Port tools sample to main (#1119)

* Port tool sample to main

* README and polish

* Port changes from main

* Fix errors

* Add branch protection
This commit is contained in:
Rob Lourens
2024-10-28 18:49:56 -07:00
committed by GitHub
parent 78baa53247
commit 2a0e869854
10 changed files with 974 additions and 281 deletions

8
chat-sample/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"search.exclude": {
"out": true
},
"git.branchProtection": [
"main"
],
}

View File

@ -8,8 +8,9 @@ When an extension uses the Chat or the Language Model API, we call it a GitHub C
This GitHub Copilot Extension sample shows:
- How to contribute a chat participant to the GitHub Copilot Chat view.
- How to contribute a simple chat participant to the GitHub Copilot Chat view.
- How to use the Language Model API to request access to the Language Model (gpt-4o, gpt-3.5-turbo, gpt-4).
- How to contribute a more sophisticated chat participant view that uses the LanguageModelTool API to contribute and invoke tools.
![demo](./demo.png)
@ -24,3 +25,11 @@ Documentation can be found here:
- Start a task `npm: watch` to compile the code
- Run the extension in a new VS Code window
- You will see the @cat chat participant show in the GitHub Copilot Chat view
## About this sample
This sample shows two different ways to build a chat participant in VS Code:
See [simple.ts](src/simple.ts) for an example of a simple chat participant that makes requests and responds to user queries. It shows how you can create chat participants with or without the [@vscode/prompt-tsx](https://www.npmjs.com/package/@vscode/prompt-tsx) library.
See [toolParticipant.ts](src/toolParticipant.ts) for an example of a chat participant that invokes tools, either dynamically or using the `toolReferences` that are attached to the request. This is a more advanced example that shows how you can use the [@vscode/prompt-tsx](https://www.npmjs.com/package/@vscode/prompt-tsx) library to implement the LLM tool calling flow and tries to implement all the features of the chat API.

View File

@ -8,13 +8,12 @@
"name": "chat-sample",
"version": "0.1.0",
"dependencies": {
"@vscode/prompt-tsx": "^0.3.0-alpha"
"@vscode/prompt-tsx": "^0.3.0-alpha.12"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@stylistic/eslint-plugin": "^2.9.0",
"@types/node": "^20",
"@types/vscode": "1.94.0",
"eslint": "^9.13.0",
"typescript": "^5.6.2",
"typescript-eslint": "^8.11.0"
@ -279,12 +278,6 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/vscode": {
"version": "1.94.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz",
"integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==",
"dev": true
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz",

View File

@ -55,6 +55,89 @@
]
}
]
},
{
"id": "chat-tools-sample.tools",
"fullName": "Tool User",
"name": "tools",
"description": "I use tools",
"isSticky": true,
"commands": [
{
"name": "list",
"description": "List all available tools"
},
{
"name": "all",
"description": "Use all registered tools. By default, only this extension's tools are used."
}
]
}
],
"languageModelTools": [
{
"name": "chat-tools-sample_tabCount",
"tags": [
"editors",
"chat-tools-sample"
],
"toolReferenceName": "tabCount",
"displayName": "Tab Count",
"modelDescription": "The number of active tabs in a tab group",
"icon": "$(files)",
"inputSchema": {
"type": "object",
"properties": {
"tabGroup": {
"type": "number",
"description": "The index of the tab group to check. This is optional- if not specified, the active tab group will be checked.",
"default": 0
}
}
}
},
{
"name": "chat-tools-sample_findFiles",
"tags": [
"files",
"search",
"chat-tools-sample"
],
"displayName": "Find Files",
"modelDescription": "Search for files in the current workspace",
"inputSchema": {
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Search for files that match this glob pattern"
}
},
"required": [
"pattern"
]
}
},
{
"name": "chat-tools-sample_runInTerminal",
"tags": [
"terminal",
"chat-tools-sample"
],
"displayName": "Run in Terminal",
"modelDescription": "Run a command in a terminal and return the output",
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The command to run"
}
},
"required": [
"command"
]
}
}
],
"commands": [
@ -72,15 +155,14 @@
"watch": "tsc -watch -p ./"
},
"dependencies": {
"@vscode/prompt-tsx": "^0.3.0-alpha"
"@vscode/prompt-tsx": "^0.3.0-alpha.12"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@stylistic/eslint-plugin": "^2.9.0",
"@types/node": "^20",
"@types/vscode": "1.94.0",
"eslint": "^9.13.0",
"typescript-eslint": "^8.11.0",
"typescript": "^5.6.2"
"typescript": "^5.6.2",
"typescript-eslint": "^8.11.0"
}
}
}

View File

@ -1,224 +1,18 @@
import { renderPrompt } from '@vscode/prompt-tsx';
import * as vscode from 'vscode';
import { PlayPrompt } from './play';
const CAT_NAMES_COMMAND_ID = 'cat.namesInEditor';
const CAT_PARTICIPANT_ID = 'chat-sample.cat';
interface ICatChatResult extends vscode.ChatResult {
metadata: {
command: string;
}
}
import { FindFilesTool, RunInTerminalTool, TabCountTool } from './tools';
import { registerToolUserChatParticipant } from './toolParticipant';
import { registerSimpleParticipant } from './simple';
export function activate(context: vscode.ExtensionContext) {
// Define a Cat chat handler.
const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<ICatChatResult> => {
// 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.
if (request.command === 'randomTeach') {
stream.progress('Picking the right topic to teach...');
const topic = getTopic(context.history);
try {
const messages = [
vscode.LanguageModelChatMessage.User('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. Always include code samples.'),
vscode.LanguageModelChatMessage.User(topic)
];
const chatResponse = await request.model.sendRequest(messages, {}, token);
for await (const fragment of chatResponse.text) {
stream.markdown(fragment);
}
} catch (err) {
handleError(logger, err, stream);
}
stream.button({
command: CAT_NAMES_COMMAND_ID,
title: vscode.l10n.t('Use Cat Names in Editor')
});
logger.logUsage('request', { kind: 'randomTeach' });
return { metadata: { command: 'randomTeach' } };
} else if (request.command === 'play') {
stream.progress('Throwing away the computer science books and preparing to play with some Python code...');
try {
// Here's an example of how to use the prompt-tsx library to build a prompt
const { messages } = await renderPrompt(
PlayPrompt,
{ userQuery: request.prompt },
{ modelMaxPromptTokens: request.model.maxInputTokens },
request.model);
const chatResponse = await request.model.sendRequest(messages, {}, token);
for await (const fragment of chatResponse.text) {
stream.markdown(fragment);
}
} catch (err) {
handleError(logger, err, stream);
}
logger.logUsage('request', { kind: 'play' });
return { metadata: { command: 'play' } };
} else {
try {
const messages = [
vscode.LanguageModelChatMessage.User(`You are a cat! Think carefully and step by step like a cat would.
Your job is to explain computer science concepts in the funny manner of a cat, using cat metaphors. Always start your response by stating what concept you are explaining. Always include code samples.`),
vscode.LanguageModelChatMessage.User(request.prompt)
];
const chatResponse = await request.model.sendRequest(messages, {}, token);
for await (const fragment of chatResponse.text) {
// Process the output from the language model
// Replace all python function definitions with cat sounds to make the user stop looking at the code and start playing with the cat
const catFragment = fragment.replaceAll('def', 'meow');
stream.markdown(catFragment);
}
} catch (err) {
handleError(logger, err, stream);
}
logger.logUsage('request', { kind: '' });
return { metadata: { command: '' } };
}
};
// Chat participants 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 cat = vscode.chat.createChatParticipant(CAT_PARTICIPANT_ID, handler);
cat.iconPath = vscode.Uri.joinPath(context.extensionUri, 'cat.jpeg');
cat.followupProvider = {
provideFollowups(result: ICatChatResult, context: vscode.ChatContext, token: vscode.CancellationToken) {
return [{
prompt: 'let us play',
label: vscode.l10n.t('Play with the cat'),
command: 'play'
} satisfies vscode.ChatFollowup];
}
};
const logger = vscode.env.createTelemetryLogger({
sendEventData(eventName, data) {
// Capture event telemetry
console.log(`Event: ${eventName}`);
console.log(`Data: ${JSON.stringify(data)}`);
},
sendErrorData(error, data) {
// Capture error telemetry
console.error(`Error: ${error}`);
console.error(`Data: ${JSON.stringify(data)}`);
}
});
context.subscriptions.push(cat.onDidReceiveFeedback((feedback: vscode.ChatResultFeedback) => {
// Log chat result feedback to be able to compute the success matric of the participant
// unhelpful / totalRequests is a good success metric
logger.logUsage('chatResultFeedback', {
kind: feedback.kind
});
}));
context.subscriptions.push(
cat,
// Register the command handler for the /meow followup
vscode.commands.registerTextEditorCommand(CAT_NAMES_COMMAND_ID, async (textEditor: vscode.TextEditor) => {
// Replace all variables in active editor with cat names and words
const text = textEditor.document.getText();
let chatResponse: vscode.LanguageModelChatResponse | undefined;
try {
// Use gpt-4o since it is fast and high quality.
const [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' });
if (!model) {
console.log('Model not found. Please make sure the GitHub Copilot Chat extension is installed and enabled.');
return;
}
const messages = [
vscode.LanguageModelChatMessage.User(`You are a cat! Think carefully and step by step like a cat would.
Your job is to replace all variable names in the following code with funny cat variable names. Be creative. IMPORTANT respond just with code. Do not use markdown!`),
vscode.LanguageModelChatMessage.User(text)
];
chatResponse = await model.sendRequest(messages, {}, new vscode.CancellationTokenSource().token);
} catch (err) {
if (err instanceof vscode.LanguageModelError) {
console.log(err.message, err.code, err.cause);
} else {
throw err;
}
return;
}
// Clear the editor content before inserting new content
await textEditor.edit(edit => {
const start = new vscode.Position(0, 0);
const end = new vscode.Position(textEditor.document.lineCount - 1, textEditor.document.lineAt(textEditor.document.lineCount - 1).text.length);
edit.delete(new vscode.Range(start, end));
});
// Stream the code into the editor as it is coming in from the Language Model
try {
for await (const fragment of chatResponse.text) {
await textEditor.edit(edit => {
const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
edit.insert(position, fragment);
});
}
} catch (err) {
// async response stream may fail, e.g network interruption or server side error
await textEditor.edit(edit => {
const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
edit.insert(position, (<Error>err).message);
});
}
}),
);
registerSimpleParticipant(context);
registerChatTools(context);
registerToolUserChatParticipant(context);
}
function handleError(logger: vscode.TelemetryLogger, err: any, stream: vscode.ChatResponseStream): void {
// making the chat request might fail because
// - model does not exist
// - user consent not given
// - quote limits exceeded
logger.logError(err);
if (err instanceof vscode.LanguageModelError) {
console.log(err.message, err.code, err.cause);
if (err.cause instanceof Error && err.cause.message.includes('off_topic')) {
stream.markdown(vscode.l10n.t('I\'m sorry, I can only explain computer science concepts.'));
}
} else {
// re-throw other errors so they show up in the UI
throw err;
}
function registerChatTools(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.lm.registerTool('chat-tools-sample_tabCount', new TabCountTool()));
context.subscriptions.push(vscode.lm.registerTool('chat-tools-sample_findFiles', new FindFilesTool()));
context.subscriptions.push(vscode.lm.registerTool('chat-tools-sample_runInTerminal', new RunInTerminalTool()));
}
// Get a random topic that the cat has not taught in the chat history yet
function getTopic(history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>): string {
const topics = ['linked list', 'recursion', 'stack', 'queue', 'pointers'];
// Filter the chat history to get only the responses from the cat
const previousCatResponses = history.filter(h => {
return h instanceof vscode.ChatResponseTurn && h.participant === CAT_PARTICIPANT_ID;
}) as vscode.ChatResponseTurn[];
// Filter the topics to get only the topics that have not been taught by the cat yet
const topicsNoRepetition = topics.filter(topic => {
return !previousCatResponses.some(catResponse => {
return catResponse.response.some(r => {
return r instanceof vscode.ChatResponseMarkdownPart && r.value.value.includes(topic);
});
});
});
return topicsNoRepetition[Math.floor(Math.random() * topicsNoRepetition.length)] || 'I have taught you everything I know. Meow!';
}
export function deactivate() { }
export function deactivate() { }

223
chat-sample/src/simple.ts Normal file
View File

@ -0,0 +1,223 @@
import { renderPrompt } from '@vscode/prompt-tsx';
import * as vscode from 'vscode';
import { PlayPrompt } from './play';
const CAT_NAMES_COMMAND_ID = 'cat.namesInEditor';
const CAT_PARTICIPANT_ID = 'chat-sample.cat';
interface ICatChatResult extends vscode.ChatResult {
metadata: {
command: string;
}
}
export function registerSimpleParticipant(context: vscode.ExtensionContext) {
// Define a Cat chat handler.
const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<ICatChatResult> => {
// 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.
if (request.command === 'randomTeach') {
stream.progress('Picking the right topic to teach...');
const topic = getTopic(context.history);
try {
const messages = [
vscode.LanguageModelChatMessage.User('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. Always include code samples.'),
vscode.LanguageModelChatMessage.User(topic)
];
const chatResponse = await request.model.sendRequest(messages, {}, token);
for await (const fragment of chatResponse.text) {
stream.markdown(fragment);
}
} catch (err) {
handleError(logger, err, stream);
}
stream.button({
command: CAT_NAMES_COMMAND_ID,
title: vscode.l10n.t('Use Cat Names in Editor')
});
logger.logUsage('request', { kind: 'randomTeach' });
return { metadata: { command: 'randomTeach' } };
} else if (request.command === 'play') {
stream.progress('Throwing away the computer science books and preparing to play with some Python code...');
try {
// Here's an example of how to use the prompt-tsx library to build a prompt
const { messages } = await renderPrompt(
PlayPrompt,
{ userQuery: request.prompt },
{ modelMaxPromptTokens: request.model.maxInputTokens },
request.model);
const chatResponse = await request.model.sendRequest(messages, {}, token);
for await (const fragment of chatResponse.text) {
stream.markdown(fragment);
}
} catch (err) {
handleError(logger, err, stream);
}
logger.logUsage('request', { kind: 'play' });
return { metadata: { command: 'play' } };
} else {
try {
const messages = [
vscode.LanguageModelChatMessage.User(`You are a cat! Think carefully and step by step like a cat would.
Your job is to explain computer science concepts in the funny manner of a cat, using cat metaphors. Always start your response by stating what concept you are explaining. Always include code samples.`),
vscode.LanguageModelChatMessage.User(request.prompt)
];
const chatResponse = await request.model.sendRequest(messages, {}, token);
for await (const fragment of chatResponse.text) {
// Process the output from the language model
// Replace all python function definitions with cat sounds to make the user stop looking at the code and start playing with the cat
const catFragment = fragment.replaceAll('def', 'meow');
stream.markdown(catFragment);
}
} catch (err) {
handleError(logger, err, stream);
}
logger.logUsage('request', { kind: '' });
return { metadata: { command: '' } };
}
};
// Chat participants 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 cat = vscode.chat.createChatParticipant(CAT_PARTICIPANT_ID, handler);
cat.iconPath = vscode.Uri.joinPath(context.extensionUri, 'cat.jpeg');
cat.followupProvider = {
provideFollowups(_result: ICatChatResult, _context: vscode.ChatContext, _token: vscode.CancellationToken) {
return [{
prompt: 'let us play',
label: vscode.l10n.t('Play with the cat'),
command: 'play'
} satisfies vscode.ChatFollowup];
}
};
const logger = vscode.env.createTelemetryLogger({
sendEventData(eventName, data) {
// Capture event telemetry
console.log(`Event: ${eventName}`);
console.log(`Data: ${JSON.stringify(data)}`);
},
sendErrorData(error, data) {
// Capture error telemetry
console.error(`Error: ${error}`);
console.error(`Data: ${JSON.stringify(data)}`);
}
});
context.subscriptions.push(cat.onDidReceiveFeedback((feedback: vscode.ChatResultFeedback) => {
// Log chat result feedback to be able to compute the success matric of the participant
// unhelpful / totalRequests is a good success metric
logger.logUsage('chatResultFeedback', {
kind: feedback.kind
});
}));
context.subscriptions.push(
cat,
// Register the command handler for the /meow followup
vscode.commands.registerTextEditorCommand(CAT_NAMES_COMMAND_ID, async (textEditor: vscode.TextEditor) => {
// Replace all variables in active editor with cat names and words
const text = textEditor.document.getText();
let chatResponse: vscode.LanguageModelChatResponse | undefined;
try {
// Use gpt-4o since it is fast and high quality.
const [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' });
if (!model) {
console.log('Model not found. Please make sure the GitHub Copilot Chat extension is installed and enabled.');
return;
}
const messages = [
vscode.LanguageModelChatMessage.User(`You are a cat! Think carefully and step by step like a cat would.
Your job is to replace all variable names in the following code with funny cat variable names. Be creative. IMPORTANT respond just with code. Do not use markdown!`),
vscode.LanguageModelChatMessage.User(text)
];
chatResponse = await model.sendRequest(messages, {}, new vscode.CancellationTokenSource().token);
} catch (err) {
if (err instanceof vscode.LanguageModelError) {
console.log(err.message, err.code, err.cause);
} else {
throw err;
}
return;
}
// Clear the editor content before inserting new content
await textEditor.edit(edit => {
const start = new vscode.Position(0, 0);
const end = new vscode.Position(textEditor.document.lineCount - 1, textEditor.document.lineAt(textEditor.document.lineCount - 1).text.length);
edit.delete(new vscode.Range(start, end));
});
// Stream the code into the editor as it is coming in from the Language Model
try {
for await (const fragment of chatResponse.text) {
await textEditor.edit(edit => {
const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
edit.insert(position, fragment);
});
}
} catch (err) {
// async response stream may fail, e.g network interruption or server side error
await textEditor.edit(edit => {
const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
edit.insert(position, (err as Error).message);
});
}
}),
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function handleError(logger: vscode.TelemetryLogger, err: any, stream: vscode.ChatResponseStream): void {
// making the chat request might fail because
// - model does not exist
// - user consent not given
// - quote limits exceeded
logger.logError(err);
if (err instanceof vscode.LanguageModelError) {
console.log(err.message, err.code, err.cause);
if (err.cause instanceof Error && err.cause.message.includes('off_topic')) {
stream.markdown(vscode.l10n.t('I\'m sorry, I can only explain computer science concepts.'));
}
} else {
// re-throw other errors so they show up in the UI
throw err;
}
}
// Get a random topic that the cat has not taught in the chat history yet
function getTopic(history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>): string {
const topics = ['linked list', 'recursion', 'stack', 'queue', 'pointers'];
// Filter the chat history to get only the responses from the cat
const previousCatResponses = history.filter(h => {
return h instanceof vscode.ChatResponseTurn && h.participant === CAT_PARTICIPANT_ID;
}) as vscode.ChatResponseTurn[];
// Filter the topics to get only the topics that have not been taught by the cat yet
const topicsNoRepetition = topics.filter(topic => {
return !previousCatResponses.some(catResponse => {
return catResponse.response.some(r => {
return r instanceof vscode.ChatResponseMarkdownPart && r.value.value.includes(topic);
});
});
});
return topicsNoRepetition[Math.floor(Math.random() * topicsNoRepetition.length)] || 'I have taught you everything I know. Meow!';
}

View File

@ -0,0 +1,138 @@
import { renderPrompt } from '@vscode/prompt-tsx';
import * as vscode from 'vscode';
import { ToolCallRound, ToolResultMetadata, ToolUserPrompt } from './toolsPrompt';
export interface TsxToolUserMetadata {
toolCallsMetadata: ToolCallsMetadata;
}
export interface ToolCallsMetadata {
toolCallRounds: ToolCallRound[];
toolCallResults: Record<string, vscode.LanguageModelToolResult>;
}
export function isTsxToolUserMetadata(obj: unknown): obj is TsxToolUserMetadata {
// If you change the metadata format, you would have to make this stricter or handle old objects in old ChatRequest metadata
return !!obj &&
!!(obj as TsxToolUserMetadata).toolCallsMetadata &&
Array.isArray((obj as TsxToolUserMetadata).toolCallsMetadata.toolCallRounds);
}
export function registerToolUserChatParticipant(context: vscode.ExtensionContext) {
const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, chatContext: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => {
if (request.command === 'list') {
stream.markdown(`Available tools: ${vscode.lm.tools.map(tool => tool.name).join(', ')}\n\n`);
return;
}
let model = request.model;
if (model.vendor === 'copilot' && model.family.startsWith('o1')) {
// The o1 models do not currently support tools
const models = await vscode.lm.selectChatModels({
vendor: 'copilot',
family: 'gpt-4o'
});
model = models[0];
}
// Use all tools, or tools with the tags that are relevant.
const tools = request.command === 'all' ?
vscode.lm.tools :
vscode.lm.tools.filter(tool => tool.tags.includes('chat-tools-sample'));
const options: vscode.LanguageModelChatRequestOptions = {
justification: 'To make a request to @toolsTSX',
};
// Render the initial prompt
const result = await renderPrompt(
ToolUserPrompt,
{
context: chatContext,
request,
toolCallRounds: [],
toolCallResults: {}
},
{ modelMaxPromptTokens: model.maxInputTokens },
model);
let messages = result.messages;
result.references.forEach(ref => {
if (ref.anchor instanceof vscode.Uri || ref.anchor instanceof vscode.Location) {
stream.reference(ref.anchor);
}
});
const toolReferences = [...request.toolReferences];
const accumulatedToolResults: Record<string, vscode.LanguageModelToolResult> = {};
const toolCallRounds: ToolCallRound[] = [];
const runWithTools = async (): Promise<void> => {
// If a toolReference is present, force the model to call that tool
const requestedTool = toolReferences.shift();
if (requestedTool) {
options.toolMode = vscode.LanguageModelChatToolMode.Required;
options.tools = vscode.lm.tools.filter(tool => tool.name === requestedTool.name);
} else {
options.toolMode = undefined;
options.tools = [...tools];
}
// Send the request to the LanguageModelChat
const response = await model.sendRequest(messages, options, token);
// Stream text output and collect tool calls from the response
const toolCalls: vscode.LanguageModelToolCallPart[] = [];
let responseStr = '';
for await (const part of response.stream) {
if (part instanceof vscode.LanguageModelTextPart) {
stream.markdown(part.value);
responseStr += part.value;
} else if (part instanceof vscode.LanguageModelToolCallPart) {
toolCalls.push(part);
}
}
if (toolCalls.length) {
// If the model called any tools, then we do another round- render the prompt with those tool calls (rendering the PromptElements will invoke the tools)
// and include the tool results in the prompt for the next request.
toolCallRounds.push({
response: responseStr,
toolCalls
});
const result = (await renderPrompt(
ToolUserPrompt,
{
context: chatContext,
request,
toolCallRounds,
toolCallResults: accumulatedToolResults
},
{ modelMaxPromptTokens: model.maxInputTokens },
model));
messages = result.messages;
const toolResultMetadata = result.metadatas.getAll(ToolResultMetadata);
if (toolResultMetadata?.length) {
// Cache tool results for later, so they can be incorporated into later prompts without calling the tool again
toolResultMetadata.forEach(meta => accumulatedToolResults[meta.toolCallId] = meta.result);
}
// This loops until the model doesn't want to call any more tools, then the request is done.
return runWithTools();
}
};
await runWithTools();
return {
metadata: {
// Return tool call metadata so it can be used in prompt history on the next request
toolCallsMetadata: {
toolCallResults: accumulatedToolResults,
toolCallRounds
}
} satisfies TsxToolUserMetadata,
};
};
const toolUser = vscode.chat.createChatParticipant('chat-tools-sample.tools', handler);
toolUser.iconPath = new vscode.ThemeIcon('tools');
context.subscriptions.push(toolUser);
}

155
chat-sample/src/tools.ts Normal file
View File

@ -0,0 +1,155 @@
import * as vscode from 'vscode';
interface ITabCountParameters {
tabGroup?: number;
}
export class TabCountTool implements vscode.LanguageModelTool<ITabCountParameters> {
async invoke(
options: vscode.LanguageModelToolInvocationOptions<ITabCountParameters>,
token: vscode.CancellationToken
) {
const params = options.input;
if (typeof params.tabGroup === 'number') {
const group = vscode.window.tabGroups.all[Math.max(params.tabGroup - 1, 0)];
const nth =
params.tabGroup === 1
? '1st'
: params.tabGroup === 2
? '2nd'
: params.tabGroup === 3
? '3rd'
: `${params.tabGroup}th`;
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`There are ${group.tabs.length} tabs open in the ${nth} tab group.`)]);
} else {
const group = vscode.window.tabGroups.activeTabGroup;
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`There are ${group.tabs.length} tabs open.`)]);
}
}
async prepareInvocation(
options: vscode.LanguageModelToolInvocationPrepareOptions<ITabCountParameters>,
token: vscode.CancellationToken
) {
const confirmationMessages = {
title: 'Count the number of open tabs',
message: new vscode.MarkdownString(
`Count the number of open tabs?` +
(options.input.tabGroup !== undefined
? ` in tab group ${options.input.tabGroup}`
: '')
),
};
return {
invocationMessage: 'Counting the number of tabs',
confirmationMessages,
};
}
}
interface IFindFilesParameters {
pattern: string;
}
export class FindFilesTool implements vscode.LanguageModelTool<IFindFilesParameters> {
async invoke(
options: vscode.LanguageModelToolInvocationOptions<IFindFilesParameters>,
token: vscode.CancellationToken
) {
const params = options.input as IFindFilesParameters;
const files = await vscode.workspace.findFiles(
params.pattern,
'**/node_modules/**',
undefined,
token
);
const strFiles = files.map((f) => f.fsPath).join('\n');
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`Found ${files.length} files matching "${params.pattern}":\n${strFiles}`)]);
}
async prepareInvocation(
options: vscode.LanguageModelToolInvocationPrepareOptions<IFindFilesParameters>,
token: vscode.CancellationToken
) {
return {
invocationMessage: `Searching workspace for "${options.input.pattern}"`,
};
}
}
interface IRunInTerminalParameters {
command: string;
}
async function waitForShellIntegration(
terminal: vscode.Terminal,
timeout: number
): Promise<void> {
let resolve: () => void;
let reject: (e: Error) => void;
let p = new Promise<void>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
const timer = setTimeout(() => reject(new Error('Could not run terminal command: shell integration is not enabled')), timeout);
const listener = vscode.window.onDidChangeTerminalShellIntegration((e) => {
if (e.terminal === terminal) {
clearTimeout(timer);
listener.dispose();
resolve();
}
});
await p;
}
export class RunInTerminalTool
implements vscode.LanguageModelTool<IRunInTerminalParameters>
{
async invoke(
options: vscode.LanguageModelToolInvocationOptions<IRunInTerminalParameters>,
token: vscode.CancellationToken
) {
const params = options.input as IRunInTerminalParameters;
const terminal = vscode.window.createTerminal('Language Model Tool User');
terminal.show();
try {
await waitForShellIntegration(terminal, 5000);
} catch(e) {
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart((e as Error).message)]);
}
const execution = terminal.shellIntegration!.executeCommand(params.command);
const terminalStream = execution.read();
let terminalResult = '';
for await (const chunk of terminalStream) {
terminalResult += chunk;
}
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(terminalResult)]);
}
async prepareInvocation(
options: vscode.LanguageModelToolInvocationPrepareOptions<IRunInTerminalParameters>,
token: vscode.CancellationToken
) {
const confirmationMessages = {
title: 'Run command in terminal',
message: new vscode.MarkdownString(
`Run this command in a terminal?` +
`\n\n\`\`\`\n${options.input.command}\n\`\`\`\n`
),
};
return {
invocationMessage: `Running command in terminal`,
confirmationMessages,
};
}
}

View File

@ -0,0 +1,280 @@
import {
AssistantMessage,
BasePromptElementProps,
Chunk,
PrioritizedList,
PromptElement,
PromptElementProps,
PromptMetadata,
PromptPiece,
PromptReference,
PromptSizing,
ToolCall,
ToolMessage,
UserMessage
} from '@vscode/prompt-tsx';
import { ToolResult } from '@vscode/prompt-tsx/dist/base/promptElements';
import * as vscode from 'vscode';
import { isTsxToolUserMetadata } from './toolParticipant';
export interface ToolCallRound {
response: string;
toolCalls: vscode.LanguageModelToolCallPart[];
}
export interface ToolUserProps extends BasePromptElementProps {
request: vscode.ChatRequest;
context: vscode.ChatContext;
toolCallRounds: ToolCallRound[];
toolCallResults: Record<string, vscode.LanguageModelToolResult>;
}
export class ToolUserPrompt extends PromptElement<ToolUserProps, void> {
render(_state: void, _sizing: PromptSizing) {
return (
<>
<UserMessage>
Instructions: <br />
- The user will ask a question, or ask you to perform a task, and it may
require lots of research to answer correctly. There is a selection of
tools that let you perform actions or retrieve helpful context to answer
the user's question. <br />
- If you aren't sure which tool is relevant, you can call multiple
tools. You can call tools repeatedly to take actions or gather as much
context as needed until you have completed the task fully. Don't give up
unless you are sure the request cannot be fulfilled with the tools you
have. <br />
- Don't make assumptions about the situation- gather context first, then
perform the task or answer the question. <br />
- Don't ask the user for confirmation to use tools, just use them.
</UserMessage>
<History context={this.props.context} priority={10} />
<PromptReferences
references={this.props.request.references}
priority={20}
/>
<UserMessage>{this.props.request.prompt}</UserMessage>
<ToolCalls
toolCallRounds={this.props.toolCallRounds}
toolInvocationToken={this.props.request.toolInvocationToken}
toolCallResults={this.props.toolCallResults}/>
</>
);
}
}
interface ToolCallsProps extends BasePromptElementProps {
toolCallRounds: ToolCallRound[];
toolCallResults: Record<string, vscode.LanguageModelToolResult>;
toolInvocationToken: vscode.ChatParticipantToolToken | undefined;
}
const dummyCancellationToken: vscode.CancellationToken = new vscode.CancellationTokenSource().token;
/**
* Render a set of tool calls, which look like an AssistantMessage with a set of tool calls followed by the associated UserMessages containing results.
*/
class ToolCalls extends PromptElement<ToolCallsProps, void> {
async render(_state: void, _sizing: PromptSizing) {
if (!this.props.toolCallRounds.length) {
return undefined;
}
// Note- for the copilot models, the final prompt must end with a non-tool-result UserMessage
return <>
{this.props.toolCallRounds.map(round => this.renderOneToolCallRound(round))}
<UserMessage>Above is the result of calling one or more tools. The user cannot see the results, so you should explain them to the user if referencing them in your answer.</UserMessage>
</>;
}
private renderOneToolCallRound(round: ToolCallRound) {
const assistantToolCalls: ToolCall[] = round.toolCalls.map(tc => ({ type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.input) }, id: tc.callId }));
return (
<Chunk>
<AssistantMessage toolCalls={assistantToolCalls}>{round.response}</AssistantMessage>
{round.toolCalls.map(toolCall =>
<ToolResultElement toolCall={toolCall} toolInvocationToken={this.props.toolInvocationToken} toolCallResult={this.props.toolCallResults[toolCall.callId]} />)}
</Chunk>);
}
}
interface ToolResultElementProps extends BasePromptElementProps {
toolCall: vscode.LanguageModelToolCallPart;
toolInvocationToken: vscode.ChatParticipantToolToken | undefined;
toolCallResult: vscode.LanguageModelToolResult | undefined;
}
/**
* One tool call result, which either comes from the cache or from invoking the tool.
*/
class ToolResultElement extends PromptElement<ToolResultElementProps, void> {
async render(state: void, sizing: PromptSizing): Promise<PromptPiece | undefined> {
const tool = vscode.lm.tools.find(t => t.name === this.props.toolCall.name);
if (!tool) {
console.error(`Tool not found: ${this.props.toolCall.name}`);
return <ToolMessage toolCallId={this.props.toolCall.callId}>Tool not found</ToolMessage>;
}
const tokenizationOptions: vscode.LanguageModelToolTokenizationOptions = {
tokenBudget: sizing.tokenBudget,
countTokens: async (content: string) => sizing.countTokens(content),
};
const toolResult = this.props.toolCallResult ??
await vscode.lm.invokeTool(this.props.toolCall.name, { input: this.props.toolCall.input, toolInvocationToken: this.props.toolInvocationToken, tokenizationOptions }, dummyCancellationToken);
return (
<ToolMessage toolCallId={this.props.toolCall.callId}>
<meta value={new ToolResultMetadata(this.props.toolCall.callId, toolResult)}></meta>
<ToolResult data={toolResult} />
</ToolMessage>
);
}
}
export class ToolResultMetadata extends PromptMetadata {
constructor(
public toolCallId: string,
public result: vscode.LanguageModelToolResult,
) {
super();
}
}
interface HistoryProps extends BasePromptElementProps {
priority: number;
context: vscode.ChatContext;
}
/**
* Render the chat history, including previous tool call/results.
*/
class History extends PromptElement<HistoryProps, void> {
render(_state: void, _sizing: PromptSizing) {
return (
<PrioritizedList priority={this.props.priority} descending={false}>
{this.props.context.history.map((message) => {
if (message instanceof vscode.ChatRequestTurn) {
return (
<>
{<PromptReferences references={message.references} excludeReferences={true} />}
<UserMessage>{message.prompt}</UserMessage>
</>
);
} else if (message instanceof vscode.ChatResponseTurn) {
const metadata = message.result.metadata;
if (isTsxToolUserMetadata(metadata) && metadata.toolCallsMetadata.toolCallRounds.length > 0) {
return <ToolCalls toolCallResults={metadata.toolCallsMetadata.toolCallResults} toolCallRounds={metadata.toolCallsMetadata.toolCallRounds} toolInvocationToken={undefined} />;
}
return <AssistantMessage>{chatResponseToString(message)}</AssistantMessage>;
}
})}
</PrioritizedList>
);
}
}
/**
* Convert the stream of chat response parts into something that can be rendered in the prompt.
*/
function chatResponseToString(response: vscode.ChatResponseTurn): string {
return response.response
.map((r) => {
if (r instanceof vscode.ChatResponseMarkdownPart) {
return r.value.value;
} else if (r instanceof vscode.ChatResponseAnchorPart) {
if (r.value instanceof vscode.Uri) {
return r.value.fsPath;
} else {
return r.value.uri.fsPath;
}
}
return '';
})
.join('');
}
interface PromptReferencesProps extends BasePromptElementProps {
references: ReadonlyArray<vscode.ChatPromptReference>;
excludeReferences?: boolean;
}
/**
* Render references that were included in the user's request, eg files and selections.
*/
class PromptReferences extends PromptElement<PromptReferencesProps, void> {
render(_state: void, _sizing: PromptSizing): PromptPiece {
return (
<UserMessage>
{this.props.references.map(ref => (
<PromptReferenceElement ref={ref} excludeReferences={this.props.excludeReferences} />
))}
</UserMessage>
);
}
}
interface PromptReferenceProps extends BasePromptElementProps {
ref: vscode.ChatPromptReference;
excludeReferences?: boolean;
}
class PromptReferenceElement extends PromptElement<PromptReferenceProps> {
async render(_state: void, _sizing: PromptSizing): Promise<PromptPiece | undefined> {
const value = this.props.ref.value;
if (value instanceof vscode.Uri) {
const fileContents = (await vscode.workspace.fs.readFile(value)).toString();
return (
<Tag name="context">
{!this.props.excludeReferences && <references value={[new PromptReference(value)]} />}
{value.fsPath}:<br />
``` <br />
{fileContents}<br />
```<br />
</Tag>
);
} else if (value instanceof vscode.Location) {
const rangeText = (await vscode.workspace.openTextDocument(value.uri)).getText(value.range);
return (
<Tag name="context">
{!this.props.excludeReferences && <references value={[new PromptReference(value)]} />}
{value.uri.fsPath}:{value.range.start.line + 1}-$<br />
{value.range.end.line + 1}: <br />
```<br />
{rangeText}<br />
```
</Tag>
);
} else if (typeof value === 'string') {
return <Tag name="context">{value}</Tag>;
}
}
}
type TagProps = PromptElementProps<{
name: string;
}>;
class Tag extends PromptElement<TagProps> {
private static readonly _regex = /^[a-zA-Z_][\w.-]*$/;
render() {
const { name } = this.props;
if (!Tag._regex.test(name)) {
throw new Error(`Invalid tag name: ${this.props.name}`);
}
return (
<>
{'<' + name + '>'}<br />
<>
{this.props.children}<br />
</>
{'</' + name + '>'}<br />
</>
);
}
}

View File

@ -19096,7 +19096,7 @@ declare module 'vscode' {
* The list of tools that the user attached to their request.
*
* When a tool reference is present, the chat participant should make a chat request using
* {@link LanguageModelChatToolMode.Required} to force the language model to generate parameters for the tool. Then, the
* {@link LanguageModelChatToolMode.Required} to force the language model to generate input for the tool. Then, the
* participant can use {@link lm.invokeTool} to use the tool attach the result to its request for the user's prompt. The
* tool may contribute useful extra context for the user's request.
*/
@ -19703,12 +19703,12 @@ declare module 'vscode' {
/**
* A list of all available tools that were registered by all extensions using {@link lm.registerTool}. They can be called
* with {@link lm.invokeTool} with a set of parameters that match their declared `parametersSchema`.
* with {@link lm.invokeTool} with input that match their declared `inputSchema`.
*/
export const tools: readonly LanguageModelToolInformation[];
/**
* Invoke a tool listed in {@link lm.tools} by name with the given parameters. The parameters will be validated against
* Invoke a tool listed in {@link lm.tools} by name with the given input. The input will be validated against
* the schema declared by the tool
*
* A tool can be invoked by a chat participant, in the context of handling a chat request, or globally by any extension in
@ -19774,9 +19774,9 @@ declare module 'vscode' {
description: string;
/**
* A JSON schema for the parameters this tool accepts.
* A JSON schema for the input this tool accepts.
*/
parametersSchema?: object;
inputSchema?: object;
}
/**
@ -19800,25 +19800,53 @@ declare module 'vscode' {
* included as a content part on a {@link LanguageModelChatMessage}, to represent a previous tool call in a chat request.
*/
export class LanguageModelToolCallPart {
/**
* The name of the tool to call.
*/
name: string;
/**
* The ID of the tool call. This is a unique identifier for the tool call within the chat request.
*/
callId: string;
/**
* The parameters with which to call the tool.
* The name of the tool to call.
*/
parameters: object;
name: string;
/**
* The input with which to call the tool.
*/
input: object;
/**
* Create a new LanguageModelToolCallPart.
*
* @param callId The ID of the tool call.
* @param name The name of the tool to call.
* @param input The input with which to call the tool.
*/
constructor(name: string, callId: string, parameters: object);
constructor(callId: string, name: string, input: object);
}
/**
* The result of a tool call. This is the counterpart of a {@link LanguageModelToolCallPart tool call} and
* it can only be included in the content of a User message
*/
export class LanguageModelToolResultPart {
/**
* The ID of the tool call.
*
* *Note* that this should match the {@link LanguageModelToolCallPart.callId callId} of a tool call part.
*/
callId: string;
/**
* The value of the tool result.
*/
content: (LanguageModelTextPart | LanguageModelPromptTsxPart | unknown)[];
/**
* @param callId The ID of the tool call.
* @param content The content of the tool result.
*/
constructor(callId: string, content: (LanguageModelTextPart | LanguageModelPromptTsxPart | unknown)[]);
}
/**
@ -19854,27 +19882,6 @@ declare module 'vscode' {
constructor(value: unknown);
}
/**
* The result of a tool call. Can only be included in the content of a User message.
*/
export class LanguageModelToolResultPart {
/**
* The ID of the tool call.
*/
callId: string;
/**
* The value of the tool result.
*/
content: (LanguageModelTextPart | LanguageModelPromptTsxPart | unknown)[];
/**
* @param callId The ID of the tool call.
* @param content The content of the tool result.
*/
constructor(callId: string, content: (LanguageModelTextPart | LanguageModelPromptTsxPart | unknown)[]);
}
/**
* A result returned from a tool invocation. If using `@vscode/prompt-tsx`, this result may be rendered using a `ToolResult`.
*/
@ -19896,27 +19903,31 @@ declare module 'vscode' {
/**
* A token that can be passed to {@link lm.invokeTool} when invoking a tool inside the context of handling a chat request.
*/
export type ChatParticipantToolToken = unknown;
export type ChatParticipantToolToken = never;
/**
* Options provided for tool invocation.
*/
export interface LanguageModelToolInvocationOptions<T> {
/**
* When this tool is being invoked by a {@link ChatParticipant} within the context of a chat request, this token should be
* passed from {@link ChatRequest.toolInvocationToken}. In that case, a progress bar will be automatically shown for the
* tool invocation in the chat response view, and if the tool requires user confirmation, it will show up inline in the
* chat view. If the tool is being invoked outside of a chat request, `undefined` should be passed instead.
* An opaque object that ties a tool invocation to a chat request from a {@link ChatParticipant chat participant}.
*
* If a tool invokes another tool during its invocation, it can pass along the `toolInvocationToken` that it received.
* The _only_ way to get a valid tool invocation token is using the provided {@link ChatRequest.toolInvocationToken toolInvocationToken}
* from a chat request. In that case, a progress bar will be automatically shown for the tool invocation in the chat response view, and if
* the tool requires user confirmation, it will show up inline in the chat view.
*
* If the tool is being invoked outside of a chat request, `undefined` should be passed instead, and no special UI except for
* confirmations will be shown.
*
* *Note* that a tool that invokes another tool during its invocation, can pass along the `toolInvocationToken` that it received.
*/
toolInvocationToken: ChatParticipantToolToken | undefined;
/**
* The parameters with which to invoke the tool. The parameters must match the schema defined in
* {@link LanguageModelToolInformation.parametersSchema}
* The input with which to invoke the tool. The input must match the schema defined in
* {@link LanguageModelToolInformation.inputSchema}
*/
parameters: T;
input: T;
/**
* Options to hint at how many tokens the tool should return in its response, and enable the tool to count tokens
@ -19958,9 +19969,9 @@ declare module 'vscode' {
readonly description: string;
/**
* A JSON schema for the parameters this tool accepts.
* A JSON schema for the input this tool accepts.
*/
readonly parametersSchema: object | undefined;
readonly inputSchema: object | undefined;
/**
* A set of tags, declared by the tool, that roughly describe the tool's capabilities. A tool user may use these to filter
@ -19974,9 +19985,9 @@ declare module 'vscode' {
*/
export interface LanguageModelToolInvocationPrepareOptions<T> {
/**
* The parameters that the tool is being invoked with.
* The input that the tool is being invoked with.
*/
parameters: T;
input: T;
}
/**
@ -19984,15 +19995,15 @@ declare module 'vscode' {
*/
export interface LanguageModelTool<T> {
/**
* Invoke the tool with the given parameters and return a result.
* Invoke the tool with the given input and return a result.
*
* The provided {@link LanguageModelToolInvocationOptions.parameters} have been validated against the declared schema.
* The provided {@link LanguageModelToolInvocationOptions.input} has been validated against the declared schema.
*/
invoke(options: LanguageModelToolInvocationOptions<T>, token: CancellationToken): ProviderResult<LanguageModelToolResult>;
/**
* Called once before a tool is invoked. It's recommended to implement this to customize the progress message that appears
* while the tool is running, and to provide a more useful message with context from the invocation parameters. Can also
* while the tool is running, and to provide a more useful message with context from the invocation input. Can also
* signal that a tool needs user confirmation before running, if appropriate.
*
* * *Note 1:* Must be free of side-effects.