Path: blob/main/extensions/copilot/src/extension/intents/node/testIntent/summarizedDocumentWithSelection.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 { PromptElement, PromptElementProps, PromptSizing, Raw } from '@vscode/prompt-tsx';6import type * as vscode from 'vscode';7import { VsCodeTextDocument } from '../../../../platform/editing/common/abstractText';8import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';9import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';10import { ILogService } from '../../../../platform/log/common/logService';11import { OverlayNode } from '../../../../platform/parser/node/nodes';12import { IParserService } from '../../../../platform/parser/node/parserService';13import { isFalsyOrWhitespace } from '../../../../util/vs/base/common/strings';14import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';15import { ServicesAccessor } from '../../../../util/vs/platform/instantiation/common/instantiation';16import { getStructure } from '../../../context/node/resolvers/selectionContextHelpers';17import { PromptMetadata } from '../../../prompt/common/conversation';18import { EarlyStopping, LeadingMarkdownStreaming, ReplyInterpreter, ReplyInterpreterMetaData } from '../../../prompt/node/intents';19import { TextPieceClassifiers } from '../../../prompt/node/streamingEdits';20import { Tag } from '../../../prompts/node/base/tag';21import { getAdjustedSelection } from '../../../prompts/node/inline/adjustSelection';22import { MarkdownBlock } from '../../../prompts/node/inline/inlineChatGenerateMarkdownPrompt';23import { SummarizedDocumentSplit } from '../../../prompts/node/inline/promptingSummarizedDocument';24import { getCharLimit, summarizeDocumentSync } from '../../../prompts/node/inline/summarizedDocument/summarizeDocumentHelpers';25import { CodeBlock, Uri, UriMode } from '../../../prompts/node/panel/safeElements';262728export enum SelectionSplitKind {29Adjusted,30OriginalEnd,31}3233function isServiceAccessor(obj: any): obj is ServicesAccessor {34return obj !== null && typeof obj === 'object' && typeof obj.get === 'function';35}3637export class SummarizedDocumentData {3839/**40* Create new summarized document data that is be used for the `SummarizedDocumentWithSelection`-element,41* the data should also be used for other parts of the prompt, e.g to know if there is selected code, etc pp42*43* @param document the document to summarize44* @param formattingOptions (optional) formatting options45* @param selection The selection or whole range46* @param selectionSplitKind Split around adjusted or original selection.47* @returns48*/49static async create(50parserService: IParserService | ServicesAccessor,51document: TextDocumentSnapshot,52formattingOptions: vscode.FormattingOptions | undefined,53selection: vscode.Range,54selectionSplitKind: SelectionSplitKind,55): Promise<SummarizedDocumentData> {5657if (isServiceAccessor(parserService)) {58parserService = parserService.get(IParserService);59}6061const structure = await getStructure(parserService, document, formattingOptions);62selection = document.validateRange(selection);63const offsetSelections = getAdjustedSelection(structure, new VsCodeTextDocument(document), selection);64return new SummarizedDocumentData(document, formattingOptions, structure, selection, offsetSelections, selectionSplitKind);65}6667readonly hasCodeWithoutSelection: boolean;68readonly hasContent: boolean;69readonly placeholderText: string;7071private constructor(72readonly document: TextDocumentSnapshot,73private readonly formattingOptions: vscode.FormattingOptions | undefined,74private readonly structure: OverlayNode,75private readonly selection: vscode.Range,76readonly offsetSelections: { adjusted: OffsetRange; original: OffsetRange },77private readonly kind: SelectionSplitKind,78) {7980const offsetSelection = kind === SelectionSplitKind.Adjusted81? offsetSelections.adjusted82: offsetSelections.original;8384const text = document.getText();85const codeSelected = text.substring(offsetSelection.start, offsetSelection.endExclusive);86const codeAbove = text.substring(0, offsetSelection.start);87const codeBelow = text.substring(offsetSelection.endExclusive);8889this.hasCodeWithoutSelection = codeAbove.trim().length > 0 || codeBelow.trim().length > 0;90this.hasContent = codeSelected.trim().length > 0 || codeAbove.trim().length > 0 || codeBelow.trim().length > 0;91this.placeholderText = offsetSelection.isEmpty ? '$PLACEHOLDER$' : '$SELECTION_PLACEHOLDER$';92}9394summarizeDocument(tokenBudget: number): SummarizedDocumentSplit {9596const doc = summarizeDocumentSync(97getCharLimit(tokenBudget),98this.document,99this.selection,100this.structure101);102103let selection: OffsetRange;104if (this.kind === SelectionSplitKind.Adjusted) {105selection = doc.projectOffsetRange(this.offsetSelections.adjusted);106} else {107selection = doc.projectOffsetRange(new OffsetRange(this.offsetSelections.original.endExclusive, this.offsetSelections.original.endExclusive));108}109110return new SummarizedDocumentSplit(111doc,112this.document.uri,113this.formattingOptions,114selection115);116}117}118119export type SummarizedDocumentWithSelectionProps = PromptElementProps<{120121/**122* The summarized document data to render.123* @see {SummarizedDocumentData.create}124*/125documentData: SummarizedDocumentData;126127/**128* The token budget to use for summarization.129*130* 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.131* So we allow just passing a number here.132*/133tokenBudget: number | 'usePromptSizingBudget';134135/**136* Optional function to create a custom `ReplyInterpreter` for the split document.137*/138createReplyInterpreter?: (splitDoc: SummarizedDocumentSplit) => ReplyInterpreter;139140_allowEmptySelection?: boolean;141}>;142143export class SummarizedDocumentSplitMetadata extends PromptMetadata {144constructor(145readonly split: SummarizedDocumentSplit,146) {147super();148}149}150151export class SummarizedDocumentWithSelection extends PromptElement<SummarizedDocumentWithSelectionProps> {152153constructor(154props: SummarizedDocumentWithSelectionProps,155@ILogService private readonly logger: ILogService,156@IIgnoreService private readonly ignoreService: IIgnoreService,157) {158super(props);159}160161override async render(_state: void, sizing: PromptSizing) {162163const { createReplyInterpreter, documentData } = this.props;164const isIgnored = await this.ignoreService.isCopilotIgnored(documentData.document.uri);165166if (isIgnored) {167return <ignoredFiles value={[documentData.document.uri]} />;168}169170let { tokenBudget } = this.props;171if (tokenBudget === 'usePromptSizingBudget') {172// some hard coded value to account for the message padding below,173// e.g the placeholder message, the path, etc174tokenBudget = (sizing.tokenBudget * .85) - 300;175}176177let splitDoc = documentData.summarizeDocument(tokenBudget);178for (let tries = 0; tries < 5; tries++) {179const text = splitDoc.codeAbove + splitDoc.codeSelected + splitDoc.codeBelow;180const actualTokens = await sizing.countTokens({ type: Raw.ChatCompletionContentPartKind.Text, text });181if (actualTokens <= tokenBudget) {182break;183}184tokenBudget *= 0.85;185splitDoc = documentData.summarizeDocument(tokenBudget);186}187188this.logger.info(`Summarized doc to fit token budget (${tokenBudget} / ${sizing.endpoint.modelMaxPromptTokens}): ${splitDoc.codeAbove.length} + ${splitDoc.codeSelected.length} + ${splitDoc.codeBelow.length}`);189190const { uri, languageId } = documentData.document;191192const isMarkdown = languageId === 'markdown';193const type = isMarkdown ? 'markdown' : 'code';194195const { codeAbove, codeSelected, codeBelow, hasCodeWithoutSelection, hasContent } = splitDoc;196197const codeWithoutSelection = `${codeAbove}${documentData.placeholderText}${codeBelow}`;198199const replyInterpreter = createReplyInterpreter200? createReplyInterpreter(splitDoc)201: splitDoc.createReplyInterpreter(202LeadingMarkdownStreaming.Mute,203EarlyStopping.StopAfterFirstCodeBlock,204splitDoc.replaceSelectionStreaming,205TextPieceClassifiers.createCodeBlockClassifier(),206line => line.value.trim() !== documentData.placeholderText207);208209return (<Tag name='currentDocument'>210<meta value={new ReplyInterpreterMetaData(replyInterpreter)} />211<meta value={new SummarizedDocumentSplitMetadata(splitDoc)} />212{!hasContent && <>I am in an empty file `<Uri value={uri} mode={UriMode.Path} />`.</>}213{hasContent && <>I have the following {type} in a file called `<Uri value={uri} mode={UriMode.Path} />`:<br /></>}214{(!isMarkdown && hasCodeWithoutSelection) && <><CodeBlock uri={uri} languageId={languageId} code={codeWithoutSelection} shouldTrim={false} /><br /></>}215{(isMarkdown && hasCodeWithoutSelection) && <><MarkdownBlock uri={uri} code={codeWithoutSelection} /><br /></>}216{217(!isFalsyOrWhitespace(codeSelected) || this.props._allowEmptySelection) &&218<Tag name='selection'>219{(!isMarkdown && hasCodeWithoutSelection) && <>The {documentData.placeholderText} code is:<br /></>}220{(isMarkdown && hasCodeWithoutSelection) && <>I need your help with the following content:</>}221{!isMarkdown && <CodeBlock uri={uri} languageId={languageId} code={codeSelected} shouldTrim={false} />}222{isMarkdown && <MarkdownBlock uri={uri} code={codeSelected} />}223</Tag>224}225</Tag>);226}227}228229230