mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-04-27 16:55:44 +08:00
286 lines
9.9 KiB
TypeScript
286 lines
9.9 KiB
TypeScript
import {
|
|
AssistantMessage,
|
|
BasePromptElementProps,
|
|
contentType as promptTsxContentType,
|
|
PrioritizedList,
|
|
PromptElement,
|
|
PromptElementProps,
|
|
PromptPiece,
|
|
PromptSizing,
|
|
UserMessage,
|
|
PromptMetadata,
|
|
ToolCall,
|
|
Chunk,
|
|
ToolMessage,
|
|
PromptReference,
|
|
TextChunk,
|
|
} from '@vscode/prompt-tsx';
|
|
import * as vscode from 'vscode';
|
|
import { isTsxToolUserMetadata } from './tsxParticipant';
|
|
import { PromptElementJSON } from '@vscode/prompt-tsx/dist/base/jsonTypes';
|
|
import { ToolResult } from '@vscode/prompt-tsx/dist/base/promptElements';
|
|
|
|
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.
|
|
<br />- 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.
|
|
</UserMessage>
|
|
<History context={this.props.context} priority={10}></History>
|
|
<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}>
|
|
</ToolCalls>
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
interface ToolCallsProps extends BasePromptElementProps {
|
|
toolCallRounds: ToolCallRound[];
|
|
toolCallResults: Record<string, vscode.LanguageModelToolResult>;
|
|
toolInvocationToken: vscode.ChatParticipantToolToken | undefined;
|
|
}
|
|
|
|
const agentSupportedContentTypes = [promptTsxContentType, 'text/plain'];
|
|
const dummyCancellationToken: vscode.CancellationToken = new vscode.CancellationTokenSource().token;
|
|
|
|
class ToolCalls extends PromptElement<ToolCallsProps, void> {
|
|
async render(state: void, sizing: PromptSizing) {
|
|
if (!this.props.toolCallRounds.length) {
|
|
return undefined;
|
|
}
|
|
|
|
// Note- the final prompt must end with a 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.parameters) }, id: tc.callId }));
|
|
// TODO- just need to adopt prompt-tsx update in vscode-copilot
|
|
return (
|
|
<Chunk>
|
|
<AssistantMessage toolCalls={assistantToolCalls}>{round.response}</AssistantMessage>
|
|
{round.toolCalls.map(toolCall =>
|
|
<ToolCallElement toolCall={toolCall} toolInvocationToken={this.props.toolInvocationToken} toolCallResult={this.props.toolCallResults[toolCall.callId]}></ToolCallElement>)}
|
|
</Chunk>);
|
|
}
|
|
}
|
|
|
|
interface ToolCallElementProps extends BasePromptElementProps {
|
|
toolCall: vscode.LanguageModelToolCallPart;
|
|
toolInvocationToken: vscode.ChatParticipantToolToken | undefined;
|
|
toolCallResult: vscode.LanguageModelToolResult | undefined;
|
|
}
|
|
|
|
class ToolCallElement extends PromptElement<ToolCallElementProps, 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, { parameters: this.props.toolCall.parameters, toolInvocationToken: this.props.toolInvocationToken, tokenizationOptions }, dummyCancellationToken);
|
|
|
|
// Important- since these parts may have been serialized/deserialized via ChatResult metadata, we need to check their types
|
|
// in a more flexible way. Extensions should not have to do this, vscode will have a better solution in the future.
|
|
toolResult.content = toolResult.content.map(part => {
|
|
if (part instanceof vscode.LanguageModelTextPart || part instanceof vscode.LanguageModelPromptTsxPart) {
|
|
return part;
|
|
} else if ((part as vscode.LanguageModelPromptTsxPart).mime) {
|
|
return new vscode.LanguageModelPromptTsxPart((part as vscode.LanguageModelPromptTsxPart).value, (part as vscode.LanguageModelPromptTsxPart).mime);
|
|
} else if (typeof (part as vscode.LanguageModelTextPart).value === 'string') {
|
|
return new vscode.LanguageModelTextPart((part as vscode.LanguageModelTextPart).value);
|
|
}
|
|
});
|
|
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;
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
class PromptReferences extends PromptElement<PromptReferencesProps, void> {
|
|
render(state: void, sizing: PromptSizing): PromptPiece {
|
|
return (
|
|
<UserMessage>
|
|
{this.props.references.map((ref, index) => (
|
|
<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;
|
|
// 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 (
|
|
<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>;
|
|
}
|
|
}
|
|
}
|
|
|
|
export type TagProps = PromptElementProps<{
|
|
name: string;
|
|
}>;
|
|
|
|
export 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 />
|
|
</>
|
|
);
|
|
}
|
|
}
|