Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/conversationHistory.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 { AssistantMessage, BasePromptElementProps, Chunk, PrioritizedList, PromptElement, PromptPiece, PromptSizing, TokenLimit, UserMessage } from '@vscode/prompt-tsx';6import { modelPrefersInstructionsAfterHistory } from '../../../../platform/endpoint/common/chatModelCapabilities';7import { URI } from '../../../../util/vs/base/common/uri';8import { Location } from '../../../../vscodeTypes';9import { ChatVariablesCollection, PromptVariable } from '../../../prompt/common/chatVariablesCollection';10import { IResultMetadata, Turn, TurnStatus } from '../../../prompt/common/conversation';11import { IBuildPromptContext } from '../../../prompt/common/intents';12import { AgentUserMessageInHistory } from '../agent/agentConversationHistory';13import { renderedMessageToTsxChildren } from '../agent/agentPrompt';14import { InstructionMessage } from '../base/instructionMessage';15import { IPromptEndpoint } from '../base/promptRenderer';16import { ChatVariablesAndQuery } from './chatVariables';17import { ChatToolCalls } from './toolCalling';1819interface ConversationHistoryProps extends BasePromptElementProps {20history: readonly Turn[];21priority: number;22/**23* Signal that is used to roll up the history into a single message, only requests24* are considered (and historical responses are assumed to be source code).25*/26inline?: boolean;27currentTurnVars?: ChatVariablesCollection;28omitPromptVariables?: boolean;29}3031/**32* This element should wrap instructions specific to any given model. It should33* include any {@link InstructionMessage}, and depending on the model it34* either includes the history before or after the instruction message.35*36* You should use `passPriority` with this: https://github.com/microsoft/vscode-prompt-tsx?tab=readme-ov-file#passing-priority37*38* @example39*40* <HistoryWithInstructions passPriority priority={700} history={history}>41* <InstructionMessage>Do the thing</InstructionMessage>42* </HistoryWithInstructions>43*/44export class HistoryWithInstructions extends PromptElement<Omit<ConversationHistoryProps, 'priority'> & { historyPriority: number }> {45constructor(46props: Omit<ConversationHistoryProps, 'priority'> & { historyPriority: number },47@IPromptEndpoint private readonly promptEndpoint: IPromptEndpoint48) {49super(props);50}51override render(_state: void, sizing: PromptSizing): PromptPiece {52const ep = this.promptEndpoint;53const { children, ...props } = this.props;54if (!children?.some(c => typeof c === 'object' && c.ctor === InstructionMessage)) {55// This is a sanity check, and could be removed if we eventually want to56// have wrappers around InstructionMessages, but for now this is useful.57throw new Error(`HistoryWithInstructions must have an InstructionMessage child`);58}5960const after = modelPrefersInstructionsAfterHistory(ep.family);61return <>62{after ? <ConversationHistory {...props} passPriority={false} priority={this.props.historyPriority} /> : undefined}63{...children}64{after ? undefined : <ConversationHistory {...props} passPriority={false} priority={this.props.historyPriority} />}65</>;66}67}6869/**70* @deprecated use `HistoryWithInstructions` instead71*/72export class ConversationHistory extends PromptElement<ConversationHistoryProps> {73override render(_state: void, _sizing: PromptSizing): PromptPiece<any, any> | undefined {74// exclude turns from the history that errored due to prompt filtration75let turnHistory = this.props.history.filter(turn => turn.responseStatus !== TurnStatus.PromptFiltered);7677if (this.props.inline && turnHistory.length > 0) {78const historyMessage = `The current code is a result of a previous interaction with you. Here are my previous messages: \n- ${turnHistory.map(r => r.request.message).join('\n- ')}`;79turnHistory = [new Turn(undefined, { message: historyMessage, type: 'user' }, undefined)];80}8182const history: (UserMessage | AssistantMessage)[] = [];83turnHistory.forEach((turn, index) => {84if (turn.request.type === 'user') {85const promptVariables = (turn.promptVariables && !this.props.omitPromptVariables) ? this.removeDuplicateVars(turn.promptVariables, this.props.currentTurnVars, turnHistory.slice(index + 1)) : new ChatVariablesCollection([]);86history.push(<ChatVariablesAndQuery priority={900} chatVariables={promptVariables} query={turn.request.message} omitReferences={true} embeddedInsideUserMessage={false} />);87}88if (turn.responseMessage?.type === 'model' && ![TurnStatus.OffTopic, TurnStatus.Filtered].includes(turn.responseStatus)) {89history.push(<AssistantMessage name={turn.responseMessage.name}>{turn.responseMessage.message}</AssistantMessage>);90}91});9293return (94// Conversation history is currently limited to 32k tokens to avoid95// unnecessarily pushing into the larger and slower token SKUs96<TokenLimit max={32768}>97<PrioritizedList priority={this.props.priority} descending={false}>{history}</PrioritizedList>98</TokenLimit>99);100}101102private removeDuplicateVars(historyVars: ChatVariablesCollection, currentTurnVars: ChatVariablesCollection | undefined, followingMessages: Turn[]): ChatVariablesCollection {103// TODO this is very simple, maybe we could use getUniqueReferences to merge ranges and be smarter. But it would take some rewriting of history for the model to104// understand what each history message was referring to.105return historyVars.filter(v1 => {106if (followingMessages.some(m => m.promptVariables?.find(v2 => variableEquals(v1, v2)))) {107return false;108}109110if (currentTurnVars?.find(v2 => variableEquals(v1, v2))) {111return false;112}113114return true;115});116}117}118119function variableEquals(v1: PromptVariable, v2: PromptVariable) {120if (v1.uniqueName !== v2.uniqueName) {121return false;122}123124if (URI.isUri(v1.value) && URI.isUri(v2.value)) {125return v1.value.toString() === v2.value.toString();126}127128if (v1.value instanceof Location && v2.value instanceof Location) {129return JSON.stringify(v1.value) === JSON.stringify(v2.value);130}131132return false;133}134135export interface ConversationHistoryWithToolsProps extends BasePromptElementProps {136readonly priority: number;137readonly promptContext: IBuildPromptContext;138}139140/**141* This is conversation history including tool calls, but not summaries. New usages should use SummarizedConversationHistory instead.142*/143export class ConversationHistoryWithTools extends PromptElement<ConversationHistoryWithToolsProps> {144override async render(state: void, sizing: PromptSizing) {145const history: PromptElement[] = [];146const contextHistory = this.props.promptContext.history;147for (const [i, turn] of contextHistory.entries()) {148const metadata = turn.responseChatResult?.metadata as IResultMetadata | undefined;149150if (metadata?.renderedUserMessage) {151history.push(<UserMessage><Chunk>{renderedMessageToTsxChildren(metadata.renderedUserMessage, false)}</Chunk></UserMessage>);152} else {153history.push(<AgentUserMessageInHistory turn={turn} />);154}155156if (Array.isArray(metadata?.toolCallRounds) && metadata.toolCallRounds?.length > 0) {157// If a tool call limit is exceeded, the tool call from this turn will158// have been aborted and any result should be found in the next turn.159const toolCallResultInNextTurn = metadata.maxToolCallsExceeded;160let toolCallResults = metadata.toolCallResults;161if (toolCallResultInNextTurn) {162const nextMetadata = contextHistory.at(i + 1)?.responseChatResult?.metadata as IResultMetadata | undefined;163const mergeFrom = i === contextHistory.length - 1 ? this.props.promptContext.toolCallResults : nextMetadata?.toolCallResults;164toolCallResults = { ...toolCallResults, ...mergeFrom };165}166167history.push(<ChatToolCalls168promptContext={this.props.promptContext}169toolCallRounds={metadata.toolCallRounds}170toolCallResults={toolCallResults}171isHistorical={!(toolCallResultInNextTurn && i === contextHistory.length - 1)}172/>);173} else if (turn.responseMessage) {174history.push(<AssistantMessage>{turn.responseMessage?.message}</AssistantMessage>);175}176}177178return (<PrioritizedList priority={this.props.priority} descending={false}>{history}</PrioritizedList>);179}180}181182183