diff --git a/chat-tools-sample/src/extension.ts b/chat-tools-sample/src/extension.ts index 7615fbb0..87f70607 100644 --- a/chat-tools-sample/src/extension.ts +++ b/chat-tools-sample/src/extension.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { FindFilesTool, RunInTerminalTool, TabCountTool } from './tools'; -import { registerToolUserChatParticipant } from './tsxParticipant'; +import { registerToolUserChatParticipant } from './participant'; export function activate(context: vscode.ExtensionContext) { registerChatTools(context); diff --git a/chat-tools-sample/src/tsxParticipant.ts b/chat-tools-sample/src/participant.ts similarity index 81% rename from chat-tools-sample/src/tsxParticipant.ts rename to chat-tools-sample/src/participant.ts index 0f8c1e96..698df475 100644 --- a/chat-tools-sample/src/tsxParticipant.ts +++ b/chat-tools-sample/src/participant.ts @@ -35,18 +35,12 @@ export function registerToolUserChatParticipant(context: vscode.ExtensionContext model = models[0]; } - const allTools = vscode.lm.tools.map((tool): vscode.LanguageModelChatTool => { - return { - name: tool.name, - description: tool.description, - parametersSchema: tool.parametersSchema ?? {} - }; - }); - + const allTools = vscode.lm.tools; const options: vscode.LanguageModelChatRequestOptions = { justification: 'To make a request to @toolsTSX', }; + // Render the initial prompt let { messages, references } = await renderPrompt( ToolUserPrompt, { @@ -66,19 +60,22 @@ export function registerToolUserChatParticipant(context: vscode.ExtensionContext const toolReferences = [...request.toolReferences]; const accumulatedToolResults: Record = {}; const toolCallRounds: ToolCallRound[] = []; - const runWithFunctions = async (): Promise => { + const runWithTools = async (): Promise => { + // 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 = allTools.filter(tool => tool.name === requestedTool.name); } else { options.toolMode = undefined; - options.tools = allTools; + options.tools = [...allTools]; } - const toolCalls: vscode.LanguageModelToolCallPart[] = []; - + // 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) { @@ -90,6 +87,8 @@ export function registerToolUserChatParticipant(context: vscode.ExtensionContext } 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 @@ -107,17 +106,20 @@ export function registerToolUserChatParticipant(context: vscode.ExtensionContext 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); } - return runWithFunctions(); + // This loops until the model doesn't want to call any more tools, then the request is done. + return runWithTools(); } }; - await runWithFunctions(); + await runWithTools(); return { metadata: { + // Return tool call metadata so it can be used in prompt history on the next request toolCallsMetadata: { toolCallResults: accumulatedToolResults, toolCallRounds @@ -126,7 +128,7 @@ export function registerToolUserChatParticipant(context: vscode.ExtensionContext } }; - const toolUser = vscode.chat.createChatParticipant('chat-tools-sample.tools2', handler); + const toolUser = vscode.chat.createChatParticipant('chat-tools-sample.tools', handler); toolUser.iconPath = new vscode.ThemeIcon('tools'); context.subscriptions.push(toolUser); } \ No newline at end of file diff --git a/chat-tools-sample/src/toolsPrompt.tsx b/chat-tools-sample/src/toolsPrompt.tsx index 9d2b8226..83fec716 100644 --- a/chat-tools-sample/src/toolsPrompt.tsx +++ b/chat-tools-sample/src/toolsPrompt.tsx @@ -16,7 +16,7 @@ import { TextChunk, } from '@vscode/prompt-tsx'; import * as vscode from 'vscode'; -import { isTsxToolUserMetadata } from './tsxParticipant'; +import { isTsxToolUserMetadata } from './participant'; import { PromptElementJSON } from '@vscode/prompt-tsx/dist/base/jsonTypes'; import { ToolResult } from '@vscode/prompt-tsx/dist/base/promptElements'; @@ -50,10 +50,8 @@ export class ToolUserPrompt extends PromptElement { - Don't make assumptions about the situation- gather context first, then perform the task or answer the question.
- Don't ask the user for confirmation to use tools, just use them. -
- After editing a file, DO NOT show the user a codeblock with the - edit or new file contents. Assume that the user can see the result. - + { - + toolCallResults={this.props.toolCallResults}/> ); } @@ -77,13 +74,16 @@ interface ToolCallsProps extends BasePromptElementProps { 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 { async render(state: void, sizing: PromptSizing) { if (!this.props.toolCallRounds.length) { return undefined; } - // Note- the final prompt must end with a UserMessage + // 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))} 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. @@ -92,23 +92,25 @@ class ToolCalls extends PromptElement { private renderOneToolCallRound(round: ToolCallRound) { const assistantToolCalls: ToolCall[] = round.toolCalls.map(tc => ({ type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.parameters) }, id: tc.callId })); - // TODO- just need to adopt prompt-tsx update in vscode-copilot return ( {round.response} {round.toolCalls.map(toolCall => - )} + )} ); } } -interface ToolCallElementProps extends BasePromptElementProps { +interface ToolResultElementProps extends BasePromptElementProps { toolCall: vscode.LanguageModelToolCallPart; toolInvocationToken: vscode.ChatParticipantToolToken | undefined; toolCallResult: vscode.LanguageModelToolResult | undefined; } -class ToolCallElement extends PromptElement { +/** + * One tool call result, which either comes from the cache or from invoking the tool. + */ +class ToolResultElement extends PromptElement { async render(state: void, sizing: PromptSizing): Promise { const tool = vscode.lm.tools.find(t => t.name === this.props.toolCall.name); if (!tool) { @@ -147,6 +149,9 @@ interface HistoryProps extends BasePromptElementProps { context: vscode.ChatContext; } +/** + * Render the chat history, including previous tool call/results. + */ class History extends PromptElement { render(state: void, sizing: PromptSizing) { return ( @@ -173,6 +178,9 @@ class History extends PromptElement { } } +/** + * 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) => { @@ -196,6 +204,9 @@ interface PromptReferencesProps extends BasePromptElementProps { excludeReferences?: boolean; } +/** + * Render references that were included in the user's request, eg files and selections. + */ class PromptReferences extends PromptElement { render(state: void, sizing: PromptSizing): PromptPiece { return ( @@ -216,7 +227,6 @@ interface PromptReferenceProps extends BasePromptElementProps { class PromptReferenceElement extends PromptElement { async render(state: void, sizing: PromptSizing): Promise { const value = this.props.ref.value; - // TODO make context a list of TextChunks so that it can be trimmed if (value instanceof vscode.Uri) { const fileContents = (await vscode.workspace.fs.readFile(value)).toString(); return ( @@ -246,11 +256,11 @@ class PromptReferenceElement extends PromptElement { } } -export type TagProps = PromptElementProps<{ +type TagProps = PromptElementProps<{ name: string; }>; -export class Tag extends PromptElement { +class Tag extends PromptElement { private static readonly _regex = /^[a-zA-Z_][\w\.\-]*$/; render() {