Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/intents/node/testIntent/summarizedDocumentWithSelection.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 { PromptElement, PromptElementProps, PromptSizing, Raw } from '@vscode/prompt-tsx';
7
import type * as vscode from 'vscode';
8
import { VsCodeTextDocument } from '../../../../platform/editing/common/abstractText';
9
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
10
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
11
import { ILogService } from '../../../../platform/log/common/logService';
12
import { OverlayNode } from '../../../../platform/parser/node/nodes';
13
import { IParserService } from '../../../../platform/parser/node/parserService';
14
import { isFalsyOrWhitespace } from '../../../../util/vs/base/common/strings';
15
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
16
import { ServicesAccessor } from '../../../../util/vs/platform/instantiation/common/instantiation';
17
import { getStructure } from '../../../context/node/resolvers/selectionContextHelpers';
18
import { PromptMetadata } from '../../../prompt/common/conversation';
19
import { EarlyStopping, LeadingMarkdownStreaming, ReplyInterpreter, ReplyInterpreterMetaData } from '../../../prompt/node/intents';
20
import { TextPieceClassifiers } from '../../../prompt/node/streamingEdits';
21
import { Tag } from '../../../prompts/node/base/tag';
22
import { getAdjustedSelection } from '../../../prompts/node/inline/adjustSelection';
23
import { MarkdownBlock } from '../../../prompts/node/inline/inlineChatGenerateMarkdownPrompt';
24
import { SummarizedDocumentSplit } from '../../../prompts/node/inline/promptingSummarizedDocument';
25
import { getCharLimit, summarizeDocumentSync } from '../../../prompts/node/inline/summarizedDocument/summarizeDocumentHelpers';
26
import { CodeBlock, Uri, UriMode } from '../../../prompts/node/panel/safeElements';
27
28
29
export enum SelectionSplitKind {
30
Adjusted,
31
OriginalEnd,
32
}
33
34
function isServiceAccessor(obj: any): obj is ServicesAccessor {
35
return obj !== null && typeof obj === 'object' && typeof obj.get === 'function';
36
}
37
38
export class SummarizedDocumentData {
39
40
/**
41
* Create new summarized document data that is be used for the `SummarizedDocumentWithSelection`-element,
42
* the data should also be used for other parts of the prompt, e.g to know if there is selected code, etc pp
43
*
44
* @param document the document to summarize
45
* @param formattingOptions (optional) formatting options
46
* @param selection The selection or whole range
47
* @param selectionSplitKind Split around adjusted or original selection.
48
* @returns
49
*/
50
static async create(
51
parserService: IParserService | ServicesAccessor,
52
document: TextDocumentSnapshot,
53
formattingOptions: vscode.FormattingOptions | undefined,
54
selection: vscode.Range,
55
selectionSplitKind: SelectionSplitKind,
56
): Promise<SummarizedDocumentData> {
57
58
if (isServiceAccessor(parserService)) {
59
parserService = parserService.get(IParserService);
60
}
61
62
const structure = await getStructure(parserService, document, formattingOptions);
63
selection = document.validateRange(selection);
64
const offsetSelections = getAdjustedSelection(structure, new VsCodeTextDocument(document), selection);
65
return new SummarizedDocumentData(document, formattingOptions, structure, selection, offsetSelections, selectionSplitKind);
66
}
67
68
readonly hasCodeWithoutSelection: boolean;
69
readonly hasContent: boolean;
70
readonly placeholderText: string;
71
72
private constructor(
73
readonly document: TextDocumentSnapshot,
74
private readonly formattingOptions: vscode.FormattingOptions | undefined,
75
private readonly structure: OverlayNode,
76
private readonly selection: vscode.Range,
77
readonly offsetSelections: { adjusted: OffsetRange; original: OffsetRange },
78
private readonly kind: SelectionSplitKind,
79
) {
80
81
const offsetSelection = kind === SelectionSplitKind.Adjusted
82
? offsetSelections.adjusted
83
: offsetSelections.original;
84
85
const text = document.getText();
86
const codeSelected = text.substring(offsetSelection.start, offsetSelection.endExclusive);
87
const codeAbove = text.substring(0, offsetSelection.start);
88
const codeBelow = text.substring(offsetSelection.endExclusive);
89
90
this.hasCodeWithoutSelection = codeAbove.trim().length > 0 || codeBelow.trim().length > 0;
91
this.hasContent = codeSelected.trim().length > 0 || codeAbove.trim().length > 0 || codeBelow.trim().length > 0;
92
this.placeholderText = offsetSelection.isEmpty ? '$PLACEHOLDER$' : '$SELECTION_PLACEHOLDER$';
93
}
94
95
summarizeDocument(tokenBudget: number): SummarizedDocumentSplit {
96
97
const doc = summarizeDocumentSync(
98
getCharLimit(tokenBudget),
99
this.document,
100
this.selection,
101
this.structure
102
);
103
104
let selection: OffsetRange;
105
if (this.kind === SelectionSplitKind.Adjusted) {
106
selection = doc.projectOffsetRange(this.offsetSelections.adjusted);
107
} else {
108
selection = doc.projectOffsetRange(new OffsetRange(this.offsetSelections.original.endExclusive, this.offsetSelections.original.endExclusive));
109
}
110
111
return new SummarizedDocumentSplit(
112
doc,
113
this.document.uri,
114
this.formattingOptions,
115
selection
116
);
117
}
118
}
119
120
export type SummarizedDocumentWithSelectionProps = PromptElementProps<{
121
122
/**
123
* The summarized document data to render.
124
* @see {SummarizedDocumentData.create}
125
*/
126
documentData: SummarizedDocumentData;
127
128
/**
129
* The token budget to use for summarization.
130
*
131
* If set to 'usePromptSizingBudget', the token budget will be read from the component's `PromptSizing` budget, which is updated only when `flex*` props are used.
132
* So we allow just passing a number here.
133
*/
134
tokenBudget: number | 'usePromptSizingBudget';
135
136
/**
137
* Optional function to create a custom `ReplyInterpreter` for the split document.
138
*/
139
createReplyInterpreter?: (splitDoc: SummarizedDocumentSplit) => ReplyInterpreter;
140
141
_allowEmptySelection?: boolean;
142
}>;
143
144
export class SummarizedDocumentSplitMetadata extends PromptMetadata {
145
constructor(
146
readonly split: SummarizedDocumentSplit,
147
) {
148
super();
149
}
150
}
151
152
export class SummarizedDocumentWithSelection extends PromptElement<SummarizedDocumentWithSelectionProps> {
153
154
constructor(
155
props: SummarizedDocumentWithSelectionProps,
156
@ILogService private readonly logger: ILogService,
157
@IIgnoreService private readonly ignoreService: IIgnoreService,
158
) {
159
super(props);
160
}
161
162
override async render(_state: void, sizing: PromptSizing) {
163
164
const { createReplyInterpreter, documentData } = this.props;
165
const isIgnored = await this.ignoreService.isCopilotIgnored(documentData.document.uri);
166
167
if (isIgnored) {
168
return <ignoredFiles value={[documentData.document.uri]} />;
169
}
170
171
let { tokenBudget } = this.props;
172
if (tokenBudget === 'usePromptSizingBudget') {
173
// some hard coded value to account for the message padding below,
174
// e.g the placeholder message, the path, etc
175
tokenBudget = (sizing.tokenBudget * .85) - 300;
176
}
177
178
let splitDoc = documentData.summarizeDocument(tokenBudget);
179
for (let tries = 0; tries < 5; tries++) {
180
const text = splitDoc.codeAbove + splitDoc.codeSelected + splitDoc.codeBelow;
181
const actualTokens = await sizing.countTokens({ type: Raw.ChatCompletionContentPartKind.Text, text });
182
if (actualTokens <= tokenBudget) {
183
break;
184
}
185
tokenBudget *= 0.85;
186
splitDoc = documentData.summarizeDocument(tokenBudget);
187
}
188
189
this.logger.info(`Summarized doc to fit token budget (${tokenBudget} / ${sizing.endpoint.modelMaxPromptTokens}): ${splitDoc.codeAbove.length} + ${splitDoc.codeSelected.length} + ${splitDoc.codeBelow.length}`);
190
191
const { uri, languageId } = documentData.document;
192
193
const isMarkdown = languageId === 'markdown';
194
const type = isMarkdown ? 'markdown' : 'code';
195
196
const { codeAbove, codeSelected, codeBelow, hasCodeWithoutSelection, hasContent } = splitDoc;
197
198
const codeWithoutSelection = `${codeAbove}${documentData.placeholderText}${codeBelow}`;
199
200
const replyInterpreter = createReplyInterpreter
201
? createReplyInterpreter(splitDoc)
202
: splitDoc.createReplyInterpreter(
203
LeadingMarkdownStreaming.Mute,
204
EarlyStopping.StopAfterFirstCodeBlock,
205
splitDoc.replaceSelectionStreaming,
206
TextPieceClassifiers.createCodeBlockClassifier(),
207
line => line.value.trim() !== documentData.placeholderText
208
);
209
210
return (<Tag name='currentDocument'>
211
<meta value={new ReplyInterpreterMetaData(replyInterpreter)} />
212
<meta value={new SummarizedDocumentSplitMetadata(splitDoc)} />
213
{!hasContent && <>I am in an empty file `<Uri value={uri} mode={UriMode.Path} />`.</>}
214
{hasContent && <>I have the following {type} in a file called `<Uri value={uri} mode={UriMode.Path} />`:<br /></>}
215
{(!isMarkdown && hasCodeWithoutSelection) && <><CodeBlock uri={uri} languageId={languageId} code={codeWithoutSelection} shouldTrim={false} /><br /></>}
216
{(isMarkdown && hasCodeWithoutSelection) && <><MarkdownBlock uri={uri} code={codeWithoutSelection} /><br /></>}
217
{
218
(!isFalsyOrWhitespace(codeSelected) || this.props._allowEmptySelection) &&
219
<Tag name='selection'>
220
{(!isMarkdown && hasCodeWithoutSelection) && <>The {documentData.placeholderText} code is:<br /></>}
221
{(isMarkdown && hasCodeWithoutSelection) && <>I need your help with the following content:</>}
222
{!isMarkdown && <CodeBlock uri={uri} languageId={languageId} code={codeSelected} shouldTrim={false} />}
223
{isMarkdown && <MarkdownBlock uri={uri} code={codeSelected} />}
224
</Tag>
225
}
226
</Tag>);
227
}
228
}
229
230