mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-06-13 07:10:26 +08:00
281 lines
9.1 KiB
TypeScript
281 lines
9.1 KiB
TypeScript
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 />
|
|
</>
|
|
);
|
|
}
|
|
}
|