123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- import { type BaseMessage, AIMessage, HumanMessage, type SystemMessage, ToolMessage } from '@langchain/core/messages';
- import { MessageHistory, MessageMetadata, ManagedMessage } from '@src/background/agent/messages/views';
- import { createLogger } from '@src/background/log';
- const logger = createLogger('MessageManager');
- export class MessageManagerSettings {
- maxInputTokens = 128000;
- estimatedCharactersPerToken = 3;
- imageTokens = 800;
- includeAttributes: string[] = [];
- messageContext?: string;
- sensitiveData?: Record<string, string>;
- availableFilePaths?: string[];
- constructor(
- options: {
- maxInputTokens?: number;
- estimatedCharactersPerToken?: number;
- imageTokens?: number;
- includeAttributes?: string[];
- messageContext?: string;
- sensitiveData?: Record<string, string>;
- availableFilePaths?: string[];
- } = {},
- ) {
- if (options.maxInputTokens !== undefined) this.maxInputTokens = options.maxInputTokens;
- if (options.estimatedCharactersPerToken !== undefined)
- this.estimatedCharactersPerToken = options.estimatedCharactersPerToken;
- if (options.imageTokens !== undefined) this.imageTokens = options.imageTokens;
- if (options.includeAttributes !== undefined) this.includeAttributes = options.includeAttributes;
- if (options.messageContext !== undefined) this.messageContext = options.messageContext;
- if (options.sensitiveData !== undefined) this.sensitiveData = options.sensitiveData;
- if (options.availableFilePaths !== undefined) this.availableFilePaths = options.availableFilePaths;
- }
- }
- export default class MessageManager {
- private history: MessageHistory;
- private toolId: number;
- private settings: MessageManagerSettings;
- constructor(settings: MessageManagerSettings = new MessageManagerSettings()) {
- this.settings = settings;
- this.history = new MessageHistory();
- this.toolId = 1;
- }
- public initTaskMessages(systemMessage: SystemMessage, task: string, messageContext?: string): void {
- // Add system message
- this.addMessageWithTokens(systemMessage, 'init');
- // Add context message if provided
- if (messageContext && messageContext.length > 0) {
- const contextMessage = new HumanMessage({
- content: `Context for the task: ${messageContext}`,
- });
- this.addMessageWithTokens(contextMessage, 'init');
- }
- // Add task instructions
- const taskMessage = MessageManager.taskInstructions(task);
- this.addMessageWithTokens(taskMessage, 'init');
- // Add sensitive data info if sensitive data is provided
- if (this.settings.sensitiveData) {
- const info = `Here are placeholders for sensitive data: ${Object.keys(this.settings.sensitiveData)}`;
- const infoMessage = new HumanMessage({
- content: `${info}\nTo use them, write <secret>the placeholder name</secret>`,
- });
- this.addMessageWithTokens(infoMessage, 'init');
- }
- // Add example output
- const placeholderMessage = new HumanMessage({
- content: 'Example output:',
- });
- this.addMessageWithTokens(placeholderMessage, 'init');
- const toolCallId = this.nextToolId();
- const toolCalls = [
- {
- name: 'AgentOutput',
- args: {
- current_state: {
- evaluation_previous_goal:
- `Success - I successfully clicked on the 'Apple' link from the Google Search results page,
- which directed me to the 'Apple' company homepage. This is a good start toward finding
- the best place to buy a new iPhone as the Apple website often list iPhones for sale.`.trim(),
- memory: `I searched for 'iPhone retailers' on Google. From the Google Search results page,
- I used the 'click_element' tool to click on a element labelled 'Best Buy' but calling
- the tool did not direct me to a new page. I then used the 'click_element' tool to click
- on a element labelled 'Apple' which redirected me to the 'Apple' company homepage.
- Currently at step 3/15.`.trim(),
- next_goal: `Looking at reported structure of the current page, I can see the item '[127]<h3 iPhone/>'
- in the content. I think this button will lead to more information and potentially prices
- for iPhones. I'll click on the link to 'iPhone' at index [127] using the 'click_element'
- tool and hope to see prices on the next page.`.trim(),
- },
- action: [{ click_element: { index: 127 } }],
- },
- id: String(toolCallId),
- type: 'tool_call' as const,
- },
- ];
- const exampleToolCall = new AIMessage({
- content: '',
- tool_calls: toolCalls,
- });
- this.addMessageWithTokens(exampleToolCall, 'init');
- this.addToolMessage('Browser started', toolCallId, 'init');
- // Add history start marker
- const historyStartMessage = new HumanMessage({
- content: '[Your task history memory starts here]',
- });
- this.addMessageWithTokens(historyStartMessage);
- // Add available file paths if provided
- if (this.settings.availableFilePaths && this.settings.availableFilePaths.length > 0) {
- const filepathsMsg = new HumanMessage({
- content: `Here are file paths you can use: ${this.settings.availableFilePaths}`,
- });
- this.addMessageWithTokens(filepathsMsg, 'init');
- }
- }
- public nextToolId(): number {
- const id = this.toolId;
- this.toolId += 1;
- return id;
- }
- /**
- * Createthe task instructions
- * @param task - The raw description of the task
- * @returns A HumanMessage object containing the task instructions
- */
- private static taskInstructions(task: string): HumanMessage {
- const content = `Your ultimate task is: """${task}""". If you achieved your ultimate task, stop everything and use the done action in the next step to complete the task. If not, continue as usual.你的所有回答都要用中文回答.`;
- return new HumanMessage({ content });
- }
- /**
- * Returns the number of messages in the history
- * @returns The number of messages in the history
- */
- public length(): number {
- return this.history.messages.length;
- }
- /**
- * Adds a new task to execute, it will be executed based on the history
- * @param newTask - The raw description of the new task
- */
- public addNewTask(newTask: string): void {
- const content = `Your new ultimate task is: """${newTask}""". Take the previous context into account and finish your new ultimate task. `;
- const msg = new HumanMessage({ content });
- this.addMessageWithTokens(msg);
- }
- /**
- * Adds a plan message to the history
- * @param plan - The raw description of the plan
- * @param position - The position to add the plan
- */
- public addPlan(plan?: string, position?: number): void {
- if (plan) {
- const msg = new AIMessage({ content: `<plan>${plan}</plan>` });
- this.addMessageWithTokens(msg, null, position);
- }
- }
- /**
- * Adds a state message to the history
- * @param stateMessage - The HumanMessage object containing the state
- */
- public addStateMessage(stateMessage: HumanMessage): void {
- this.addMessageWithTokens(stateMessage);
- }
- /**
- * Adds a model output message to the history
- * @param modelOutput - The model output
- */
- public addModelOutput(modelOutput: Record<string, any>): void {
- const toolCallId = this.nextToolId();
- const toolCalls = [
- {
- name: 'AgentOutput',
- args: modelOutput,
- id: String(toolCallId),
- type: 'tool_call' as const,
- },
- ];
- const msg = new AIMessage({
- content: 'tool call',
- tool_calls: toolCalls,
- });
- this.addMessageWithTokens(msg);
- // Need a placeholder for the tool response here to avoid errors sometimes
- // NOTE: in browser-use, it uses an empty string
- this.addToolMessage('tool call response placeholder', toolCallId);
- }
- /**
- * Removes the last state message from the history
- */
- public removeLastStateMessage(): void {
- this.history.removeLastStateMessage();
- }
- public getMessages(): BaseMessage[] {
- const messages = this.history.messages.map(m => m.message);
- let totalInputTokens = 0;
- logger.debug(`Messages in history: ${this.history.messages.length}:`);
- for (const m of this.history.messages) {
- totalInputTokens += m.metadata.tokens;
- logger.debug(`${m.message.constructor.name} - Token count: ${m.metadata.tokens}`);
- }
- logger.debug(`Total input tokens: ${totalInputTokens}`);
- return messages;
- }
- /**
- * Adds a message to the history with the token count metadata
- * @param message - The BaseMessage object to add
- * @param messageType - The type of the message (optional)
- * @param position - The optional position to add the message, if not provided, the message will be added to the end of the history
- */
- public addMessageWithTokens(message: BaseMessage, messageType?: string | null, position?: number): void {
- let filteredMessage = message;
- // filter out sensitive data if provided
- if (this.settings.sensitiveData) {
- filteredMessage = this._filterSensitiveData(message);
- }
- const tokenCount = this._countTokens(filteredMessage);
- const metadata: MessageMetadata = new MessageMetadata(tokenCount, messageType);
- this.history.addMessage(filteredMessage, metadata, position);
- }
- /**
- * Filters out sensitive data from the message
- * @param message - The BaseMessage object to filter
- * @returns The filtered BaseMessage object
- */
- private _filterSensitiveData(message: BaseMessage): BaseMessage {
- const replaceSensitive = (value: string): string => {
- let filteredValue = value;
- if (!this.settings.sensitiveData) return filteredValue;
- for (const [key, val] of Object.entries(this.settings.sensitiveData)) {
- // Skip empty values to match Python behavior
- if (!val) continue;
- filteredValue = filteredValue.replace(val, `<secret>${key}</secret>`);
- }
- return filteredValue;
- };
- if (typeof message.content === 'string') {
- message.content = replaceSensitive(message.content);
- } else if (Array.isArray(message.content)) {
- message.content = message.content.map(item => {
- // Add null check to match Python's isinstance() behavior
- if (typeof item === 'object' && item !== null && 'text' in item) {
- return { ...item, text: replaceSensitive(item.text) };
- }
- return item;
- });
- }
- return message;
- }
- /**
- * Counts the tokens in the message
- * @param message - The BaseMessage object to count the tokens
- * @returns The number of tokens in the message
- */
- private _countTokens(message: BaseMessage): number {
- let tokens = 0;
- if (Array.isArray(message.content)) {
- for (const item of message.content) {
- if ('image_url' in item) {
- tokens += this.settings.imageTokens;
- } else if (typeof item === 'object' && 'text' in item) {
- tokens += this._countTextTokens(item.text);
- }
- }
- } else {
- let msg = message.content;
- // Check if it's an AIMessage with tool_calls
- if ('tool_calls' in message) {
- msg += JSON.stringify(message.tool_calls);
- }
- tokens += this._countTextTokens(msg);
- }
- return tokens;
- }
- /**
- * Counts the tokens in the text
- * Rough estimate, no tokenizer provided for now
- * @param text - The text to count the tokens
- * @returns The number of tokens in the text
- */
- private _countTextTokens(text: string): number {
- return Math.floor(text.length / this.settings.estimatedCharactersPerToken);
- }
- /**
- * Cuts the last message if the total tokens exceed the max input tokens
- *
- * Get current message list, potentially trimmed to max tokens
- */
- public cutMessages(): void {
- let diff = this.history.totalTokens - this.settings.maxInputTokens;
- if (diff <= 0) return;
- const lastMsg = this.history.messages[this.history.messages.length - 1];
- // if list with image remove image
- if (Array.isArray(lastMsg.message.content)) {
- let text = '';
- lastMsg.message.content = lastMsg.message.content.filter(item => {
- if ('image_url' in item) {
- diff -= this.settings.imageTokens;
- lastMsg.metadata.tokens -= this.settings.imageTokens;
- this.history.totalTokens -= this.settings.imageTokens;
- logger.debug(
- `Removed image with ${this.settings.imageTokens} tokens - total tokens now: ${this.history.totalTokens}/${this.settings.maxInputTokens}`,
- );
- return false;
- }
- if ('text' in item) {
- text += item.text;
- }
- return true;
- });
- lastMsg.message.content = text;
- this.history.messages[this.history.messages.length - 1] = lastMsg;
- }
- if (diff <= 0) return;
- // if still over, remove text from state message proportionally to the number of tokens needed with buffer
- // Calculate the proportion of content to remove
- const proportionToRemove = diff / lastMsg.metadata.tokens;
- if (proportionToRemove > 0.99) {
- throw new Error(
- `Max token limit reached - history is too long - reduce the system prompt or task. proportion_to_remove: ${proportionToRemove}`,
- );
- }
- logger.debug(
- `Removing ${(proportionToRemove * 100).toFixed(2)}% of the last message (${(proportionToRemove * lastMsg.metadata.tokens).toFixed(2)} / ${lastMsg.metadata.tokens.toFixed(2)} tokens)`,
- );
- const content = lastMsg.message.content as string;
- const charactersToRemove = Math.floor(content.length * proportionToRemove);
- const newContent = content.slice(0, -charactersToRemove);
- // remove tokens and old long message
- this.history.removeLastStateMessage();
- // new message with updated content
- const msg = new HumanMessage({ content: newContent });
- this.addMessageWithTokens(msg);
- const finalMsg = this.history.messages[this.history.messages.length - 1];
- logger.debug(
- `Added message with ${finalMsg.metadata.tokens} tokens - total tokens now: ${this.history.totalTokens}/${this.settings.maxInputTokens} - total messages: ${this.history.messages.length}`,
- );
- }
- /**
- * Adds a tool message to the history
- * @param content - The content of the tool message
- * @param toolCallId - The tool call id of the tool message, if not provided, a new tool call id will be generated
- * @param messageType - The type of the tool message
- */
- public addToolMessage(content: string, toolCallId?: number, messageType?: string | null): void {
- const id = toolCallId ?? this.nextToolId();
- const msg = new ToolMessage({ content, tool_call_id: String(id) });
- this.addMessageWithTokens(msg, messageType);
- }
- }
|