feat: Add FileBlock object (#3488)

This commit is contained in:
Matthew Zhou
2025-07-22 15:41:21 -07:00
committed by GitHub
parent fa58214a99
commit 6252e78e67
6 changed files with 31 additions and 16 deletions

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from letta.constants import FILE_IS_TRUNCATED_WARNING
from letta.orm.mixins import OrganizationMixin
from letta.orm.sqlalchemy_base import SqlalchemyBase
from letta.schemas.block import Block as PydanticBlock
from letta.schemas.block import FileBlock as PydanticFileBlock
from letta.schemas.file import FileAgent as PydanticFileAgent
if TYPE_CHECKING:
@@ -59,7 +59,7 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
String,
ForeignKey("sources.id", ondelete="CASCADE"),
nullable=False,
doc="ID of the source (denormalized from files.source_id)",
doc="ID of the source",
)
file_name: Mapped[str] = mapped_column(
@@ -86,7 +86,7 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
)
# TODO: This is temporary as we figure out if we want FileBlock as a first class citizen
def to_pydantic_block(self, per_file_view_window_char_limit: int) -> PydanticBlock:
def to_pydantic_block(self, per_file_view_window_char_limit: int) -> PydanticFileBlock:
visible_content = self.visible_content if self.visible_content and self.is_open else ""
# Truncate content and add warnings here when converting from FileAgent to Block
@@ -95,10 +95,13 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
visible_content = visible_content[: per_file_view_window_char_limit - len(truncated_warning)]
visible_content += truncated_warning
return PydanticBlock(
return PydanticFileBlock(
value=visible_content,
label=self.file_name, # use denormalized file_name instead of self.file.file_name
label=self.file_name,
read_only=True,
metadata={"source_id": self.source_id}, # use denormalized source_id
file_id=self.file_id,
source_id=self.source_id,
is_open=self.is_open,
last_accessed_at=self.last_accessed_at,
limit=per_file_view_window_char_limit,
)

View File

@@ -370,7 +370,7 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
"{% endif %}"
"{% if file_blocks %}"
"{% for block in file_blocks %}"
"{% if block.metadata and block.metadata.get('source_id') == source.id %}"
"{% if block.source_id and block.source_id == source.id %}"
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\">\n"
"<{{ block.label }}>\n"
"<description>\n"
@@ -437,7 +437,7 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
"{% endif %}"
"{% if file_blocks %}"
"{% for block in file_blocks %}"
"{% if block.metadata and block.metadata.get('source_id') == source.id %}"
"{% if block.source_id and block.source_id == source.id %}"
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\" name=\"{{{{ block.label }}}}\">\n"
"{% if block.description %}"
"<description>\n"
@@ -503,7 +503,7 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
"{% endif %}"
"{% if file_blocks %}"
"{% for block in file_blocks %}"
"{% if block.metadata and block.metadata.get('source_id') == source.id %}"
"{% if block.source_id and block.source_id == source.id %}"
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\" name=\"{{{{ block.label }}}}\">\n"
"{% if block.description %}"
"<description>\n"

View File

@@ -1,3 +1,4 @@
from datetime import datetime
from typing import Optional
from pydantic import Field, model_validator
@@ -79,6 +80,16 @@ class Block(BaseBlock):
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that last updated this Block.")
class FileBlock(Block):
file_id: str = Field(..., description="Unique identifier of the file.")
source_id: str = Field(..., description="Unique identifier of the source.")
is_open: bool = Field(..., description="True if the agent currently has the file open.")
last_accessed_at: Optional[datetime] = Field(
default_factory=datetime.utcnow,
description="UTC timestamp of the agents most recent access to this file. Any operations from the open, close, or search tools will update this field.",
)
class Human(Block):
"""Human block of the LLM context"""

View File

@@ -67,7 +67,7 @@ class FileAgentBase(LettaBase):
# Core file-agent association fields
agent_id: str = Field(..., description="Unique identifier of the agent.")
file_id: str = Field(..., description="Unique identifier of the file.")
source_id: str = Field(..., description="Unique identifier of the source (denormalized from files.source_id).")
source_id: str = Field(..., description="Unique identifier of the source.")
file_name: str = Field(..., description="Name of the file.")
is_open: bool = Field(True, description="True if the agent currently has the file open.")
visible_content: Optional[str] = Field(

View File

@@ -11,7 +11,7 @@ if TYPE_CHECKING:
from openai.types.beta.function_tool import FunctionTool as OpenAITool
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
from letta.schemas.block import Block
from letta.schemas.block import Block, FileBlock
from letta.schemas.message import Message
@@ -66,8 +66,8 @@ class Memory(BaseModel, validate_assignment=True):
# Memory.block contains the list of memory blocks in the core memory
blocks: List[Block] = Field(..., description="Memory blocks contained in the agent's in-context memory")
file_blocks: List[Block] = Field(
default_factory=list, description="Blocks representing the agent's in-context memory of an attached file"
file_blocks: List[FileBlock] = Field(
default_factory=list, description="Special blocks representing the agent's in-context memory of an attached file"
)
@field_validator("file_blocks")

View File

@@ -1,5 +1,5 @@
from datetime import datetime, timezone
from typing import List, Optional
from typing import List, Optional, Union
from sqlalchemy import and_, func, select, update
@@ -8,6 +8,7 @@ from letta.orm.errors import NoResultFound
from letta.orm.files_agents import FileAgent as FileAgentModel
from letta.otel.tracing import trace_method
from letta.schemas.block import Block as PydanticBlock
from letta.schemas.block import FileBlock as PydanticFileBlock
from letta.schemas.file import FileAgent as PydanticFileAgent
from letta.schemas.file import FileMetadata
from letta.schemas.user import User as PydanticUser
@@ -231,7 +232,7 @@ class FileAgentManager:
actor: PydanticUser,
is_open_only: bool = False,
return_as_blocks: bool = False,
) -> List[PydanticFileAgent]:
) -> Union[List[PydanticFileAgent], List[PydanticFileBlock]]:
"""Return associations for *agent_id* (filtering by `is_open` if asked)."""
async with db_registry.async_session() as session:
conditions = [
@@ -351,7 +352,7 @@ class FileAgentManager:
agent_id: ID of the agent
file_id: ID of the file to open
file_name: Name of the file to open
source_id: ID of the source (denormalized from files.source_id)
source_id: ID of the source
actor: User performing the action
visible_content: Content to set for the opened file