mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-06-13 07:10:26 +08:00
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:
8
chat-sample/.vscode/settings.json
vendored
Normal file
8
chat-sample/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"out": true
|
||||
},
|
||||
"git.branchProtection": [
|
||||
"main"
|
||||
],
|
||||
}
|
||||
@ -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.
|
||||
|
||||

|
||||
|
||||
@ -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.
|
||||
9
chat-sample/package-lock.json
generated
9
chat-sample/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
223
chat-sample/src/simple.ts
Normal 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!';
|
||||
}
|
||||
138
chat-sample/src/toolParticipant.ts
Normal file
138
chat-sample/src/toolParticipant.ts
Normal 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
155
chat-sample/src/tools.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
280
chat-sample/src/toolsPrompt.tsx
Normal file
280
chat-sample/src/toolsPrompt.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
111
chat-sample/vscode.d.ts
vendored
111
chat-sample/vscode.d.ts
vendored
@ -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.
|
||||
|
||||
Reference in New Issue
Block a user