From 499bdcbf5f09c7efcc67072761c8ad7011c399cb Mon Sep 17 00:00:00 2001 From: Matthew Zhou Date: Mon, 30 Jun 2025 11:35:03 -0700 Subject: [PATCH] fix: Fix duplicate file blocks (#3105) --- letta/schemas/memory.py | 27 +++++++++++++++++++++++++- letta/services/agent_manager.py | 4 +++- letta/services/files_agents_manager.py | 7 +++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/letta/schemas/memory.py b/letta/schemas/memory.py index f18fdd4e..8437bd04 100644 --- a/letta/schemas/memory.py +++ b/letta/schemas/memory.py @@ -1,7 +1,8 @@ +import logging from typing import TYPE_CHECKING, List, Optional from jinja2 import Template, TemplateSyntaxError -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator # Forward referencing to avoid circular import with Agent -> Memory -> Agent if TYPE_CHECKING: @@ -69,6 +70,30 @@ class Memory(BaseModel, validate_assignment=True): default_factory=list, description="Blocks representing the agent's in-context memory of an attached file" ) + @field_validator("file_blocks") + @classmethod + def validate_file_blocks_no_duplicates(cls, v: List[Block]) -> List[Block]: + """Validate that file_blocks don't contain duplicate labels, log warnings and remove duplicates.""" + if not v: + return v + + seen_labels = set() + unique_blocks = [] + duplicate_labels = [] + + for block in v: + if block.label in seen_labels: + duplicate_labels.append(block.label) + else: + seen_labels.add(block.label) + unique_blocks.append(block) + + if duplicate_labels: + logger = logging.getLogger(__name__) + logger.warning(f"Duplicate block labels found in file_blocks: {duplicate_labels}. Removing duplicates.") + + return unique_blocks + # Memory.template is a Jinja2 template for compiling memory module into a prompt string. prompt_template: str = Field( default="{% for block in blocks %}" diff --git a/letta/services/agent_manager.py b/letta/services/agent_manager.py index 0e2256f1..c2710a0c 100644 --- a/letta/services/agent_manager.py +++ b/letta/services/agent_manager.py @@ -1730,7 +1730,9 @@ class AgentManager: agent_state.memory.blocks = [b for b in blocks if b is not None] if file_block_names: - file_blocks = await self.file_agent_manager.get_all_file_blocks_by_name(file_names=file_block_names, actor=actor) + file_blocks = await self.file_agent_manager.get_all_file_blocks_by_name( + file_names=file_block_names, agent_id=agent_state.id, actor=actor + ) agent_state.memory.file_blocks = [b for b in file_blocks if b is not None] return agent_state diff --git a/letta/services/files_agents_manager.py b/letta/services/files_agents_manager.py index b6831fb3..6ed3e052 100644 --- a/letta/services/files_agents_manager.py +++ b/letta/services/files_agents_manager.py @@ -165,17 +165,19 @@ class FileAgentManager: self, *, file_names: List[str], + agent_id: str, actor: PydanticUser, ) -> List[PydanticBlock]: """ - Retrieve multiple FileAgent associations by their IDs in a single query. + Retrieve multiple FileAgent associations by their file names for a specific agent. Args: file_names: List of file names to retrieve + agent_id: ID of the agent to retrieve file blocks for actor: The user making the request Returns: - List of PydanticFileAgent objects found (may be fewer than requested if some IDs don't exist) + List of PydanticBlock objects found (may be fewer than requested if some file names don't exist) """ if not file_names: return [] @@ -185,6 +187,7 @@ class FileAgentManager: query = select(FileAgentModel).where( and_( FileAgentModel.file_name.in_(file_names), + FileAgentModel.agent_id == agent_id, FileAgentModel.organization_id == actor.organization_id, ) )