Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/chatVariables.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, PromptElementProps, PromptPiece, PromptReference, PromptSizing, TextChunk, UserMessage } from '@vscode/prompt-tsx';6import type { Diagnostic, LanguageModelToolInformation } from 'vscode';7import { ChatFetchResponseType, ChatLocation } from '../../../../platform/chat/common/commonTypes';8import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';9import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';10import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';11import { FileType } from '../../../../platform/filesystem/common/fileTypes';12import { ILogService } from '../../../../platform/log/common/logService';13import { ICopilotToolCall } from '../../../../platform/networking/common/fetch';14import { IChatEndpoint } from '../../../../platform/networking/common/networking';15import { IAlternativeNotebookContentService } from '../../../../platform/notebook/common/alternativeContent';16import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService';17import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';18import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';19import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';20import { getLanguage, getLanguageForResource } from '../../../../util/common/languages';21import { createFencedCodeBlock } from '../../../../util/common/markdown';22import { getNotebookAndCellFromUri } from '../../../../util/common/notebooks';23import { isLocation } from '../../../../util/common/types';24import { CancellationToken } from '../../../../util/vs/base/common/cancellation';25import { Schemas } from '../../../../util/vs/base/common/network';26import { isEqual } from '../../../../util/vs/base/common/resources';27import { URI } from '../../../../util/vs/base/common/uri';28import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';29import { DiagnosticSeverity } from '../../../../util/vs/workbench/api/common/extHostTypes/diagnostic';30import { ChatReferenceBinaryData, ChatReferenceDiagnostic, LanguageModelToolResult2, Range, Uri } from '../../../../vscodeTypes';31import { GenericBasePromptElementProps } from '../../../context/node/resolvers/genericPanelIntentInvocation';32import { ChatVariablesCollection, isCustomizationsIndex, isInstructionFile, isPromptFile, isSessionReference, parseSlashCommand, sessionReferenceAttachmentAttrs } from '../../../prompt/common/chatVariablesCollection';33import { InternalToolReference } from '../../../prompt/common/intents';34import { ToolName } from '../../../tools/common/toolNames';35import { normalizeToolSchema } from '../../../tools/common/toolSchemaNormalizer';36import { IToolsService } from '../../../tools/common/toolsService';37import { EmbeddedInsideUserMessage, embeddedInsideUserMessageDefault } from '../base/promptElement';38import { IPromptEndpoint, PromptRenderer } from '../base/promptRenderer';39import { Tag } from '../base/tag';40import { DiagnosticSuggestedFix } from '../inline/diagnosticsContext';41import { Cookbook, IFixCookbookService } from '../inline/fixCookbookService';42import { SummarizedDocumentLineNumberStyle } from '../inline/summarizedDocument/implementation';43import { FilePathMode, FileVariable } from './fileVariable';44import { Image } from './image';45import { NotebookCellOutputVariable } from './notebookVariables';46import { PanelChatBasePrompt } from './panelChatBasePrompt';47import { PromptFile } from './promptFile';48import { sendInvokedToolTelemetry, toolCallErrorToResult, ToolResult, ToolResultMetadata } from './toolCalling';49import { IFileTreeData, workspaceVisualFileTree } from './workspace/visualFileTree';5051export interface ChatVariablesProps extends BasePromptElementProps, EmbeddedInsideUserMessage {52readonly chatVariables: ChatVariablesCollection;53readonly includeFilepath?: boolean;54readonly omitReferences?: boolean;55readonly isAgent?: boolean;56readonly useFixCookbook?: boolean;57/**58* If true, file attachment contents are omitted and only the file names/paths are included.59*/60readonly omitFileContents?: boolean;61}6263export class ChatVariables extends PromptElement<ChatVariablesProps, void> {64constructor(65props: ChatVariablesProps,66@IFileSystemService private readonly fileSystemService: IFileSystemService,67@IConfigurationService private readonly configurationService: IConfigurationService,68@IExperimentationService private readonly experimentationService: IExperimentationService,69) {70super(props);71}7273override async render(state: void, sizing: PromptSizing): Promise<PromptPiece<any, any> | undefined> {74// Only check experiment setting for agent mode75const omitFileContents = this.props.omitFileContents ?? (this.props.isAgent && this.configurationService.getExperimentBasedConfig(ConfigKey.Advanced.AgentOmitFileAttachmentContents, this.experimentationService));76const elements = await renderChatVariables(this.props.chatVariables, this.fileSystemService, this.props.includeFilepath, true, this.props.omitReferences, this.props.isAgent, this.props.useFixCookbook, omitFileContents);77if (elements.length === 0) {78return undefined;79}8081if (this.props.embeddedInsideUserMessage ?? embeddedInsideUserMessageDefault) {82return (83<>84{Boolean(elements.length) && <Tag name='attachments' priority={this.props.priority}>85{...elements}86</Tag>}87</>88);89}90return (<>{...elements.map(element => asUserMessage(element, this.props.priority))}</>);91}92}9394export interface QueryProps extends BasePromptElementProps {95readonly chatVariables: ChatVariablesCollection;96readonly query: string;97}9899export class UserQuery extends PromptElement<QueryProps, void> {100constructor(101props: PromptElementProps<QueryProps>,102) {103super(props);104}105106override render(state: void, sizing: PromptSizing): PromptPiece<any, any> | undefined {107const promptFiles: PromptElement[] = [];108for (const v of this.props.chatVariables) {109if (isPromptFile(v)) {110promptFiles.push(<PromptFile variable={v} omitReferences={false} />);111}112}113114const userMessage = buildSlashCommandUserMessage(this.props.query, this.props.chatVariables);115116return (117<>118{...promptFiles}119{userMessage}120</>121);122}123}124125/**126* Builds the user message for a slash command query. If the query matches a slash command127* that corresponds to a prompt file, returns an instruction to follow that prompt file128* (with any trailing arguments). Otherwise, returns the original query.129*/130export function buildSlashCommandUserMessage(query: string, chatVariables: ChatVariablesCollection): string {131const match = parseSlashCommand(query, chatVariables);132if (match) {133return match.args134? `Follow instructions in #${match.promptFile.name} with these arguments: ${match.args}`135: `Follow instructions in #${match.promptFile.name}`;136}137return query;138}139140export interface ChatVariablesAndQueryProps extends BasePromptElementProps, EmbeddedInsideUserMessage {141readonly query: string;142readonly chatVariables: ChatVariablesCollection;143/**144* By default, the chat variables are reversed. Set this to true to maintain the variable order.145*/146readonly maintainOrder?: boolean;147readonly includeFilepath?: boolean;148readonly omitReferences?: boolean;149/**150* If true, file attachment contents are omitted and only the file names/paths are included.151*/152readonly omitFileContents?: boolean;153}154155export class ChatVariablesAndQuery extends PromptElement<ChatVariablesAndQueryProps, void> {156constructor(157props: ChatVariablesAndQueryProps,158@IFileSystemService private readonly fileSystemService: IFileSystemService,159) {160super(props);161}162163override async render(state: void, sizing: PromptSizing): Promise<PromptPiece<any, any> | undefined> {164const chatVariables = this.props.maintainOrder ? this.props.chatVariables : this.props.chatVariables.reverse();165const elements = await renderChatVariables(chatVariables, this.fileSystemService, this.props.includeFilepath, true, this.props.omitReferences, undefined, undefined, this.props.omitFileContents);166167if (this.props.embeddedInsideUserMessage ?? embeddedInsideUserMessageDefault) {168if (!elements.length) {169return (170<Tag name='prompt'>171<UserQuery chatVariables={chatVariables} query={this.props.query} priority={this.props.priority} />172</Tag>173);174}175return (<>176{Boolean(elements.length) && <Tag name='attachments' flexGrow={1} priority={this.props.priority}>177{elements}178</Tag>}179<Tag name='prompt'>180<UserQuery chatVariables={chatVariables} query={this.props.query} priority={this.props.priority} />181</Tag>182</>);183}184185return (<>186{...elements.map(element => asUserMessage(element, this.props.priority && this.props.priority - 1))}187{asUserMessage(<UserQuery chatVariables={chatVariables} query={this.props.query} />, this.props.priority)}188</>);189}190}191192function asUserMessage(element: PromptElement, priority: number | undefined): UserMessage {193return (<UserMessage priority={priority}>{element}</UserMessage>);194}195196197export async function renderChatVariables(chatVariables: ChatVariablesCollection, fileSystemService: IFileSystemService, includeFilepathInCodeBlocks = true, alwaysIncludeSummary = true, omitReferences?: boolean, isAgent?: boolean, useFixCookbook?: boolean, omitFileContents?: boolean): Promise<PromptElement[]> {198const elements = [];199const filePathMode = (isAgent && includeFilepathInCodeBlocks)200? FilePathMode.AsAttribute201: includeFilepathInCodeBlocks202? FilePathMode.AsComment203: FilePathMode.None;204for (const variable of chatVariables) {205const { uniqueName: variableName, value: variableValue, reference } = variable;206if (isInstructionFile(variable) || isCustomizationsIndex(variable) || isPromptFile(variable)) { // instructions and index are handled in the `CustomInstructions` element, prompt file as part of the UserQuery207continue;208}209210if (isSessionReference(variable)) {211elements.push(<Tag name='attachment' attrs={sessionReferenceAttachmentAttrs(variable)} />);212continue;213}214215if (URI.isUri(variableValue) || isLocation(variableValue)) {216const uri = 'uri' in variableValue ? variableValue.uri : variableValue;217218// Check if the variable is a directory219let isDirectory = false;220try {221const stat = await fileSystemService.stat(uri);222isDirectory = stat.type === FileType.Directory;223} catch { }224225if (isDirectory) {226elements.push(<FolderVariable variableName={variableName} folderUri={uri} omitReferences={omitReferences} description={reference.modelDescription} omitContents={omitFileContents} />);227} else {228const file = <FileVariable229alwaysIncludeSummary={alwaysIncludeSummary}230filePathMode={filePathMode}231variableName={variableName}232variableValue={variableValue}233omitReferences={omitReferences}234description={reference.modelDescription}235lineNumberStyle={isAgent ? SummarizedDocumentLineNumberStyle.OmittedRanges : undefined}236omitContents={omitFileContents}237/>;238239if (!isAgent || (!URI.isUri(variableValue) || variableValue.scheme !== Schemas.vscodeNotebookCellOutput)) {240// When attaching outupts, there's no need to add the entire notebook file again, as model can request the notebook file.241// In non agent mode, we need to add the file for context.242elements.push(file);243}244if (URI.isUri(variableValue) && variableValue.scheme === Schemas.vscodeNotebookCellOutput) {245elements.push(<NotebookCellOutputVariable outputUri={variableValue} />);246}247}248} else if (typeof variableValue === 'string') {249elements.push(250<Tag name='attachment' attrs={variableName ? { id: variableName } : undefined} >251<TextChunk>252{!omitReferences && <references value={[new PromptReference({ variableName })]} />}253{reference.modelDescription ? reference.modelDescription + ':\n' : ''}254{variableValue}255</TextChunk>256</Tag>257);258} else if (variableValue instanceof ChatReferenceBinaryData) {259elements.push(<Image variableName={variableName} variableValue={await variableValue.data()} reference={variableValue.reference} omitReferences={omitReferences}></Image>);260} else if (typeof ChatReferenceDiagnostic !== 'undefined' && variableValue instanceof ChatReferenceDiagnostic) { // check undefined to avoid breaking old Insiders versions261elements.push(<DiagnosticVariable diagnostics={variableValue.diagnostics} useCookbook={useFixCookbook ?? false} />);262}263}264return elements;265}266267interface IDiagnosticVariableProps extends BasePromptElementProps {268diagnostics: [uri: Uri, diagnostics: Diagnostic[]][];269useCookbook?: boolean;270// useRelatedInfo?: boolean;271}272273const diagnosticSeverityMap: { [K in DiagnosticSeverity]: string } = {274[DiagnosticSeverity.Error]: 'error',275[DiagnosticSeverity.Warning]: 'warning',276[DiagnosticSeverity.Information]: 'info',277[DiagnosticSeverity.Hint]: 'hint'278};279280class DiagnosticVariable extends PromptElement<IDiagnosticVariableProps> {281constructor(282props: PromptElementProps<IDiagnosticVariableProps>,283@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,284@IWorkspaceService private readonly workspaceService: IWorkspaceService,285@IFixCookbookService private readonly fixCookbookService: IFixCookbookService,286@IAlternativeNotebookContentService private readonly alternativeNotebookContent: IAlternativeNotebookContentService,287@IPromptEndpoint private readonly endpoint: IPromptEndpoint,288) {289super(props);290}291292render() {293return <>294{this.props.diagnostics.flatMap(([uri, diagnostics]) =>295diagnostics.map(d => {296let range = d.range;297([uri, range] = this.translateNotebookUri(uri, range));298299let cookbook: Cookbook | undefined;300if (this.props.useCookbook) {301const doc = this.workspaceService.textDocuments.find(doc => isEqual(doc.uri, uri));302const lang = doc ? getLanguage(doc) : getLanguageForResource(uri);303cookbook = this.fixCookbookService.getCookbook(lang.languageId, d);304}305306return <>307<Tag name='error' attrs={{ path: this.promptPathRepresentationService.getFilePath(uri), line: range.start.line + 1, code: getDiagnosticCode(d), severity: diagnosticSeverityMap[d.severity] }}>308{d.message}309</Tag>310{cookbook && <DiagnosticSuggestedFix cookbook={cookbook} />}311</>;312}313)314)}315</>;316}317private translateNotebookUri(uri: Uri, range: Range): [Uri, Range] {318if (uri.scheme !== Schemas.vscodeNotebookCell) {319return [uri, range];320}321const [notebook, cell] = getNotebookAndCellFromUri(uri, this.workspaceService.notebookDocuments);322if (!notebook || !cell) {323return [uri, range];324}325if (range.start.line > cell.document.lineCount || range.end.line > cell.document.lineCount) {326return [uri, range];327}328329const altDocument = this.alternativeNotebookContent.create(this.alternativeNotebookContent.getFormat(this.endpoint)).getAlternativeDocument(notebook);330const start = altDocument.fromCellPosition(cell, range.start);331const end = altDocument.fromCellPosition(cell, range.end);332const newRange = new Range(start, end);333return [notebook.uri, newRange];334}335}336337function getDiagnosticCode(diagnostic: Diagnostic): string {338const code = (typeof diagnostic.code === 'object' && !!diagnostic.code) ? diagnostic.code.value : diagnostic.code;339return String(code);340}341342interface IFolderVariableProps extends BasePromptElementProps {343variableName: string;344folderUri: Uri;345omitReferences?: boolean;346description?: string;347/**348* If true, folder contents (file tree) are omitted and only the folder path is included.349*/350omitContents?: boolean;351}352353class FolderVariable extends PromptElement<IFolderVariableProps, IFileTreeData | undefined> {354constructor(355props: PromptElementProps<IFolderVariableProps>,356@IInstantiationService private readonly instantiationService: IInstantiationService,357@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,358) {359super(props);360}361362override async prepare(sizing: PromptSizing): Promise<IFileTreeData | undefined> {363if (this.props.omitContents) {364// Skip fetching the file tree when contents are omitted365return undefined;366}367try {368return this.instantiationService.invokeFunction(accessor =>369workspaceVisualFileTree(accessor, this.props.folderUri, { maxLength: 2000, excludeDotFiles: false }, CancellationToken.None)370);371} catch {372// Directory doesn't exist or is not accessible373return undefined;374}375}376377render(state: IFileTreeData | undefined) {378const folderPath = this.promptPathRepresentationService.getFilePath(this.props.folderUri);379if (this.props.omitContents) {380return (381<Tag name='attachment' attrs={this.props.variableName ? { id: this.props.variableName, folderPath } : undefined} />382);383}384return (385<Tag name='attachment' attrs={this.props.variableName ? { id: this.props.variableName, folderPath } : undefined}>386<TextChunk>387{!this.props.omitReferences && <references value={[new PromptReference({ variableName: this.props.variableName })]} />}388{this.props.description ? this.props.description + ':\n' : ''}389The user attached the folder `{folderPath}`{state ? ' which has the following structure: ' + createFencedCodeBlock('', state.tree) : ''}390</TextChunk>391</Tag>392);393}394}395396export interface ChatToolCallProps extends GenericBasePromptElementProps, EmbeddedInsideUserMessage {397}398399interface IToolCallResult {400readonly name: string | undefined;401readonly value: LanguageModelToolResult2;402}403404/**405* Render toolReferences set on the request.406*/407export class ChatToolReferences extends PromptElement<ChatToolCallProps, void> {408constructor(409props: ChatToolCallProps,410@IInstantiationService private readonly instantiationService: IInstantiationService,411@IToolsService private readonly toolsService: IToolsService,412@ILogService private readonly logService: ILogService,413@IEndpointProvider private readonly endpointProvider: IEndpointProvider,414@IPromptEndpoint private readonly promptEndpoint: IPromptEndpoint,415@ITelemetryService private readonly telemetryService: ITelemetryService416) {417super(props);418}419420override async render(state: void, sizing: PromptSizing, _progress: unknown, token?: CancellationToken): Promise<PromptPiece<any, any> | undefined> {421const { tools, toolCallResults } = this.props.promptContext;422if (!tools || !tools.toolReferences.length) {423return;424}425426const results: IToolCallResult[] = [];427for (const toolReference of tools.toolReferences) {428const tool = this.toolsService.getTool(toolReference.name);429if (!tool) {430throw new Error(`Unknown tool: "${toolReference.name}"`);431}432433if (toolCallResults?.[toolReference.id]) {434results.push({ name: toolReference.name, value: toolCallResults[toolReference.id] });435continue;436}437438const toolArgsEndpoint = await this.endpointProvider.getChatEndpoint('copilot-fast');439const internalToolArgs = toolReference.input ?? {};440const toolArgs = await this.fetchToolArgs(tool, toolArgsEndpoint);441442const name = toolReference.range ? this.props.promptContext.query.slice(toolReference.range[0], toolReference.range[1]) : undefined;443try {444const result = await this.toolsService.invokeToolWithEndpoint(tool.name, { input: { ...toolArgs, ...internalToolArgs }, toolInvocationToken: tools.toolInvocationToken }, this.promptEndpoint, token || CancellationToken.None);445sendInvokedToolTelemetry(this.promptEndpoint.acquireTokenizer(), this.telemetryService, tool.name, result);446results.push({ name, value: result });447} catch (err) {448const errResult = toolCallErrorToResult(err);449results.push({ name, value: errResult.result });450}451}452453if (this.props.embeddedInsideUserMessage ?? embeddedInsideUserMessageDefault) {454return this._renderChatToolResults(tools.toolReferences, results, this.props.priority);455}456457return (458<UserMessage priority={this.props.priority}>459{this._renderChatToolResults(tools.toolReferences, results)}460</UserMessage>461);462}463464private _renderChatToolResults(tools: readonly InternalToolReference[], results: readonly IToolCallResult[], priority?: number) {465return (466<>467These attachments may have useful context for the user's query. The user may refer to these attachments directly using a term that starts with #.<br />468{...results.map((toolResult, i) => this.renderChatToolResult(tools[i].id, toolResult, priority))}469</>470);471}472473private renderChatToolResult(id: string, toolResult: IToolCallResult, priority?: number): PromptElement {474return <Tag name='attachment' attrs={toolResult.name ? { tool: toolResult.name } : undefined} priority={priority}>475<meta value={new ToolResultMetadata(id, toolResult.value)}></meta>476<ToolResult content={toolResult.value.content} toolCallId={id} sessionId={this.props.promptContext.request?.sessionId} />477</Tag>;478}479480private async fetchToolArgs(tool: LanguageModelToolInformation, endpoint: IChatEndpoint): Promise<any> {481const ownTool = this.toolsService.getCopilotTool(tool.name as ToolName);482if (typeof ownTool?.provideInput === 'function') {483const input = await ownTool.provideInput(this.props.promptContext);484if (input) {485return input;486}487}488489if (!tool.inputSchema || Object.keys(tool.inputSchema).length === 0) {490return {};491}492493const argFetchProps: GenericBasePromptElementProps = {494...this.props,495promptContext: {496...this.props.promptContext,497tools: undefined498}499};500const toolTokens = await endpoint.acquireTokenizer().countToolTokens([tool]);501const { messages } = await PromptRenderer.create(this.instantiationService, { ...endpoint, modelMaxPromptTokens: endpoint.modelMaxPromptTokens - toolTokens }, PanelChatBasePrompt, argFetchProps).render();502let fnCall: ICopilotToolCall | undefined;503const fetchResult = await endpoint.makeChatRequest(504'fetchToolArgs',505messages,506async (text, _, delta) => {507if (delta.copilotToolCalls) {508fnCall = delta.copilotToolCalls[0];509}510return undefined;511},512CancellationToken.None,513ChatLocation.Panel,514undefined,515{516tools: normalizeToolSchema(517endpoint.family,518[519{520type: 'function',521function: {522name: tool.name,523description: tool.description,524parameters: tool.inputSchema525}526}527],528(tool, rule) => this.logService.warn(`Tool ${tool} failed validation: ${rule}`)529),530tool_choice: {531type: 'function',532function: {533name: tool.name,534}535},536},537false538);539if (!fnCall) {540throw new Error(`Failed to compute args for tool: "${tool.name}"`);541}542543if (fetchResult.type !== ChatFetchResponseType.Success) {544throw new Error(`Fetching tool args failed: ${fetchResult.type} ${fetchResult.reason}`);545}546547try {548const args = JSON.parse(fnCall.arguments);549return args;550} catch (e) {551throw new Error('Invalid tool arguments: ' + e.message);552}553}554}555556557