Path: blob/main/extensions/copilot/src/extension/prompts/node/agent/copilotCLIPrompt.tsx
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { BasePromptElementProps, PromptElement, PromptSizing, UserMessage } from '@vscode/prompt-tsx';6import { ChatCompletionContentPartKind, ChatRole } from '@vscode/prompt-tsx/dist/base/output/rawTypes';7import type { ChatRequestEditedFileEvent } from 'vscode';8import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';9import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';10import { IChatEndpoint } from '../../../../platform/networking/common/networking';11import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService';12import { isLocation } from '../../../../util/common/types';13import { Schemas } from '../../../../util/vs/base/common/network';14import { URI } from '../../../../util/vs/base/common/uri';15import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';16import { ChatReferenceBinaryData, ChatRequest, FileType } from '../../../../vscodeTypes';17import { ChatVariablesCollection, isPromptFile, isSessionReference, PromptVariable, sessionReferenceAttachmentAttrs } from '../../../prompt/common/chatVariablesCollection';18import { renderPromptElement } from '../base/promptRenderer';19import { Tag } from '../base/tag';20import { SummarizedDocumentLineNumberStyle } from '../inline/summarizedDocument/implementation';21import { renderChatVariables } from '../panel/chatVariables';22import { FilePathMode, FileVariable } from '../panel/fileVariable';23import { EditedFileEvents } from './agentPrompt';24import './allAgentPrompts';2526export interface AgentUserMessageProps extends BasePromptElementProps {27readonly request: string;28readonly endpoint: IChatEndpoint;29readonly chatVariables: ChatVariablesCollection;30readonly editedFileEvents?: readonly ChatRequestEditedFileEvent[];31readonly sessionId?: string;32}3334/**35* Is sent with each user message. Includes the user message and also any ambient context that we want to update with each request.36* Uses frozen content if available, for prompt caching and to avoid being updated by any agent action below this point in the conversation.37*/38class CopilotCLIAgentUserMessage extends PromptElement<AgentUserMessageProps> {39constructor(40props: AgentUserMessageProps,41@IFileSystemService private readonly fileSystemService: IFileSystemService,42@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,43) {44super(props);45}4647async render(state: void, sizing: PromptSizing) {48const query = this.props.request;49const shouldUseUserQuery = this.props.endpoint.family.startsWith('grok-code');5051// Files & folders will not be added as regular attachments, as those will be handed by SDK.52// We merely add a <attachments> tag to signal that there are file/folder attachments.53// This is because we want to avoid adding all fo the content of the file into the prompt.54// We leave that for Copilot CLI SDK to handle.55const isResourceVariable = (variable: PromptVariable) =>56!isScmEntry(variable.value) && (URI.isUri(variable.value) || isLocation(variable.value));57const isImageReference = (variable: PromptVariable) => variable.value && variable.value instanceof ChatReferenceBinaryData;58const isImageReferenceWithUri = (variable: PromptVariable) => variable.value && variable.value instanceof ChatReferenceBinaryData && !!variable.value.reference ? true : false;5960const resourceVariables = this.props.chatVariables.filter(variable => isResourceVariable(variable) || isImageReferenceWithUri(variable));61const nonResourceVariables = this.props.chatVariables.filter(variable => !isResourceVariable(variable) && !isImageReference(variable));62const [nonResourceAttachments, resourceAttachments] = await Promise.all([63renderChatVariables(nonResourceVariables, this.fileSystemService, true, false, false, true, false),64renderResourceVariables(resourceVariables, this.fileSystemService, this.promptPathRepresentationService)65]);6667const hasVariables = resourceVariables.hasVariables() || nonResourceVariables.hasVariables();68const hasEditedFileEvents = (this.props.editedFileEvents?.length ?? 0) > 0;69const hasCustomContext = hasVariables || hasEditedFileEvents;70const promptVariable = resourceVariables.find(v => isPromptFile(v));71// If we have a prompt file, we want to direct the model to follow instructions in that file.72// Otherwise we add a generic reminder to only use the context if its relevant.73// Also today we have a generic prompt that reads `Implement this.` and we have attachments.74// Thats not sufficient to direct the model to use prompt instructions.75// In regular chat we have `Follow instructions in #<file>` & thats very effective as the prompt is very sepcfici about what to do. `Implement this.` is not.76const instructions = promptVariable && promptVariable.reference.name !== 'prompt:plan.prompt.md' ?77`Follow instructions in #${promptVariable.reference.name}` :78'IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task';79return (80<UserMessage>81{/**82* We need to ensure the user request is first, else CLI will not be able parse this for display in summary.83* The `<reminder>` tag is a hack so that we can add additional context without interfering with the main user request.84* CLI will ignore `<reminder>` content for summary purposes.85* This is why we place it after the main user request.86*/}87<>{this.props.request}</>88{89hasCustomContext && (90<>91<br /> {/** Add an empty line after user prompt to ensure `<reminder>` tag is on a new line */}92<Tag name='reminder'>93{instructions}94</Tag>95</>96)97}98{99hasVariables &&100<Tag name='attachments' priority={this.props.priority}>101{...nonResourceAttachments}102{...resourceAttachments}103</Tag>104}105{106hasEditedFileEvents &&107<Tag name='context'>108<EditedFileEvents editedFileEvents={this.props.editedFileEvents} />109</Tag>110}111{hasCustomContext && <Tag name={shouldUseUserQuery ? 'user_query' : 'userRequest'} priority={900} flexGrow={7}>{query}</Tag>}112</UserMessage>113);114}115}116117export async function generateUserPrompt(request: ChatRequest, prompt: string | undefined, chatVariables: ChatVariablesCollection, instantiationService: IInstantiationService): Promise<string> {118const endpoint = await instantiationService.invokeFunction((accessor) => accessor.get(IEndpointProvider).getChatEndpoint(request));119const { messages } = await renderPromptElement(instantiationService, endpoint, CopilotCLIAgentUserMessage, {120chatVariables,121endpoint,122request: prompt ?? request.prompt,123editedFileEvents: request.editedFileEvents,124});125126const userMessages = messages.filter(message => message.role === ChatRole.User);127if (userMessages.length > 0) {128const textParts = userMessages.flatMap(message => message.content);129if (textParts.every(part => part.type === ChatCompletionContentPartKind.Text)) {130return textParts.map(part => part.text).join('');131}132}133throw new Error(`[CopilotCLISession] Unexpected generated prompt structure.`);134135}136137async function renderResourceVariables(chatVariables: ChatVariablesCollection, fileSystemService: IFileSystemService, promptPathRepresentationService: IPromptPathRepresentationService): Promise<PromptElement[]> {138const elements: PromptElement[] = [];139await Promise.all(Array.from(chatVariables).map(async variable => {140if (isSessionReference(variable)) {141elements.push(<Tag name='attachment' attrs={sessionReferenceAttachmentAttrs(variable)} />);142return;143}144if (variable.value instanceof ChatReferenceBinaryData) {145if (variable.value.reference) {146const attrs: Record<string, string> = {};147const variableName = variable.uniqueName;148if (variableName) {149attrs.id = variableName;150}151attrs.filePath = promptPathRepresentationService.getFilePath(variable.value.reference);152elements.push(<Tag name='attachment' attrs={attrs} />);153}154return;155}156const location = variable.value;157if (isLocation(location)) {158// If its an untitled document, we always include a summary, as CLI cannot read untitled documents.159const alwaysIncludeSummary = location.uri.scheme === Schemas.untitled;160elements.push(<FileVariable161alwaysIncludeSummary={alwaysIncludeSummary}162filePathMode={FilePathMode.AsComment}163variableName={variable.uniqueName}164variableValue={location}165description={variable.reference.modelDescription}166lineNumberStyle={SummarizedDocumentLineNumberStyle.OmittedRanges}167/>);168return;169}170const uri = variable.value;171if (!URI.isUri(uri)) {172return;173}174if (uri.scheme === Schemas.untitled || isPromptFile(variable) || isScmEntry(uri)) {175// If its an untitled document, we always include a summary, as CLI cannot read untitled documents.176// Similarly prompt file contents need to be included in the prompt.177// Except when its attached as a regular file (but in that case `isPromptFile` would return false).178elements.push(<FileVariable179alwaysIncludeSummary={true}180filePathMode={FilePathMode.AsComment}181variableName={variable.uniqueName}182variableValue={uri}183description={variable.reference.modelDescription}184lineNumberStyle={SummarizedDocumentLineNumberStyle.OmittedRanges}185/>);186return;187}188// Check if the variable is a directory189let isDirectory = false;190try {191const stat = await fileSystemService.stat(uri);192isDirectory = stat.type === FileType.Directory;193} catch { }194const attrs: Record<string, string> = {};195const variableName = variable.uniqueName;196if (variableName) {197attrs.id = variableName;198}199if (isDirectory) {200attrs.folderPath = promptPathRepresentationService.getFilePath(uri);201} else {202attrs.filePath = promptPathRepresentationService.getFilePath(uri);203}204elements.push(<Tag name='attachment' attrs={attrs} />);205}));206return elements;207}208209function isScmEntry(item: unknown): item is URI {210if (URI.isUri(item) && item.scheme === 'scm-history-item') {211return true;212}213return false;214}215216217