mirror of
https://github.com/browser-use/browser-use
synced 2026-05-06 17:52:15 +02:00
190 lines
7.2 KiB
Python
190 lines
7.2 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import warnings
|
|
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
|
|
|
from langchain_core.language_models.chat_models import BaseChatModel
|
|
from langchain_core.messages import (
|
|
BaseMessage,
|
|
HumanMessage,
|
|
)
|
|
from langchain_core.messages.utils import convert_to_openai_messages
|
|
|
|
from browser_use.agent.memory.views import MemoryConfig
|
|
from browser_use.agent.message_manager.service import MessageManager
|
|
from browser_use.agent.message_manager.views import ManagedMessage, MessageMetadata
|
|
from browser_use.config import CONFIG
|
|
from browser_use.utils import time_execution_sync
|
|
|
|
|
|
class Memory:
|
|
"""
|
|
Manages procedural memory for agents.
|
|
|
|
This class implements a procedural memory management system using Mem0 that transforms agent interaction history
|
|
into concise, structured representations at specified intervals. It serves to optimize context window
|
|
utilization during extended task execution by converting verbose historical information into compact,
|
|
yet comprehensive memory constructs that preserve essential operational knowledge.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message_manager: MessageManager,
|
|
llm: BaseChatModel,
|
|
config: MemoryConfig | None = None,
|
|
logger: logging.Logger | None = None,
|
|
):
|
|
self.message_manager = message_manager
|
|
self.llm = llm
|
|
self.logger = logger or logging.getLogger(__name__)
|
|
|
|
# Initialize configuration with defaults based on the LLM if not provided
|
|
if config is None:
|
|
self.config = MemoryConfig(llm_instance=llm, agent_id=f'agent_{id(self)}')
|
|
|
|
# Set appropriate embedder based on LLM type
|
|
llm_class = llm.__class__.__name__
|
|
if llm_class == 'ChatOpenAI':
|
|
self.config.embedder_provider = 'openai'
|
|
self.config.embedder_model = 'text-embedding-3-small'
|
|
self.config.embedder_dims = 1536
|
|
elif llm_class == 'ChatGoogleGenerativeAI':
|
|
self.config.embedder_provider = 'gemini'
|
|
self.config.embedder_model = 'models/text-embedding-004'
|
|
self.config.embedder_dims = 768
|
|
elif llm_class == 'ChatOllama':
|
|
self.config.embedder_provider = 'ollama'
|
|
self.config.embedder_model = 'nomic-embed-text'
|
|
self.config.embedder_dims = 512
|
|
else:
|
|
# Ensure LLM instance is set in the config
|
|
self.config = MemoryConfig.model_validate(config) # revalidate using Pydantic
|
|
self.config.llm_instance = llm
|
|
|
|
# Check for required packages
|
|
try:
|
|
# also disable mem0's telemetry when ANONYMIZED_TELEMETRY=False
|
|
if not CONFIG.ANONYMIZED_TELEMETRY:
|
|
os.environ['MEM0_TELEMETRY'] = 'False'
|
|
from mem0 import Memory as Mem0Memory
|
|
except ImportError:
|
|
raise ImportError('mem0 is required when enable_memory=True. Please install it with `pip install mem0`.')
|
|
|
|
if self.config.embedder_provider == 'huggingface':
|
|
try:
|
|
# check that required package is installed if huggingface is used
|
|
from sentence_transformers import SentenceTransformer # noqa: F401 # type: ignore
|
|
except ImportError:
|
|
raise ImportError(
|
|
'sentence_transformers is required when enable_memory=True and embedder_provider="huggingface". Please install it with `pip install sentence-transformers`.'
|
|
)
|
|
|
|
# Initialize Mem0 with the configuration
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
|
try:
|
|
self.mem0 = Mem0Memory.from_config(config_dict=self.config.full_config_dict)
|
|
except Exception as e:
|
|
if 'history_old' in str(e) and 'sqlite3.OperationalError' in str(type(e)):
|
|
# Handle the migration error by using a unique history database path
|
|
import tempfile
|
|
import uuid
|
|
|
|
self.logger.warning(
|
|
f'⚠️ Mem0 SQLite migration error detected in {self.config.full_config_dict}. Using a temporary database to avoid conflicts.\n{type(e).__name__}: {e}'
|
|
)
|
|
# Create a unique temporary database path
|
|
temp_dir = tempfile.gettempdir()
|
|
unique_id = str(uuid.uuid4())[:8]
|
|
history_db_path = os.path.join(temp_dir, f'browser_use_mem0_history_{unique_id}.db')
|
|
|
|
# Add the history_db_path to the config
|
|
config_with_history_path = self.config.full_config_dict.copy()
|
|
config_with_history_path['history_db_path'] = history_db_path
|
|
|
|
# Try again with the new config
|
|
self.mem0 = Mem0Memory.from_config(config_dict=config_with_history_path)
|
|
else:
|
|
# Re-raise if it's a different error
|
|
raise
|
|
|
|
@time_execution_sync('--create_procedural_memory')
|
|
def create_procedural_memory(self, current_step: int) -> None:
|
|
"""
|
|
Create a procedural memory if needed based on the current step.
|
|
|
|
Args:
|
|
current_step: The current step number of the agent
|
|
"""
|
|
self.logger.debug(f'📜 Creating procedural memory at step {current_step}')
|
|
|
|
# Get all messages
|
|
all_messages = self.message_manager.state.history.messages
|
|
|
|
# Separate messages into those to keep as-is and those to process for memory
|
|
new_messages = []
|
|
messages_to_process = []
|
|
|
|
for msg in all_messages:
|
|
if isinstance(msg, ManagedMessage) and msg.metadata.message_type in {'init', 'memory'}:
|
|
# Keep system and memory messages as they are
|
|
new_messages.append(msg)
|
|
else:
|
|
if len(msg.message.content) > 0:
|
|
messages_to_process.append(msg)
|
|
|
|
# Need at least 2 messages to create a meaningful summary
|
|
if len(messages_to_process) <= 1:
|
|
self.logger.debug('📜 Not enough non-memory messages to summarize')
|
|
return
|
|
# Create a procedural memory with timeout
|
|
try:
|
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
future = executor.submit(self._create, [m.message for m in messages_to_process], current_step)
|
|
memory_content = future.result(timeout=5)
|
|
except TimeoutError:
|
|
self.logger.warning('📜 Procedural memory creation timed out after 30 seconds')
|
|
return
|
|
except Exception as e:
|
|
self.logger.error(f'📜 Error during procedural memory creation: {e}')
|
|
return
|
|
|
|
if not memory_content:
|
|
self.logger.warning('📜 Failed to create procedural memory')
|
|
return
|
|
|
|
# Replace the processed messages with the consolidated memory
|
|
memory_message = HumanMessage(content=memory_content)
|
|
memory_tokens = self.message_manager._count_tokens(memory_message)
|
|
memory_metadata = MessageMetadata(tokens=memory_tokens, message_type='memory')
|
|
|
|
# Calculate the total tokens being removed
|
|
removed_tokens = sum(m.metadata.tokens for m in messages_to_process)
|
|
|
|
# Add the memory message
|
|
new_messages.append(ManagedMessage(message=memory_message, metadata=memory_metadata))
|
|
|
|
# Update the history
|
|
self.message_manager.state.history.messages = new_messages
|
|
self.message_manager.state.history.current_tokens -= removed_tokens
|
|
self.message_manager.state.history.current_tokens += memory_tokens
|
|
self.logger.info(f'📜 History consolidated: {len(messages_to_process)} steps converted to long-term memory')
|
|
|
|
def _create(self, messages: list[BaseMessage], current_step: int) -> str | None:
|
|
parsed_messages = convert_to_openai_messages(messages)
|
|
try:
|
|
results = self.mem0.add(
|
|
messages=parsed_messages,
|
|
agent_id=self.config.agent_id,
|
|
memory_type='procedural_memory',
|
|
metadata={'step': current_step},
|
|
)
|
|
if len(results.get('results', [])):
|
|
return results.get('results', [])[0].get('memory')
|
|
return None
|
|
except Exception as e:
|
|
self.logger.error(f'📜 Error creating procedural memory: {e}')
|
|
return None
|