feat: Add FileBlock object (#3488)
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 agent’s 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"""
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user