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