Files
letta-server/letta/schemas/block.py
Kian Jones 9a8d381b14 fix: add field validators to Block schema for int32 limit and null bytes (#9185)
- Add `validate_limit_int32` validator (mode=after) to reject limit values
  exceeding PostgreSQL INTEGER max (2147483647)
- Add `sanitize_value_null_bytes` validator (mode=before) to strip null bytes
  from block values, preventing asyncpg CharacterNotInRepertoireError

Fixes DB errors:
- "value out of int32 range" on block.limit
- "invalid byte sequence for encoding UTF8: 0x00" on block.value

🤖 Generated with [Letta Code](https://letta.com)

Co-authored-by: Letta <noreply@letta.com>
2026-02-24 10:52:05 -08:00

243 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
from typing import Any, List, Optional
from pydantic import ConfigDict, Field, field_validator, model_validator
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT, DEFAULT_HUMAN_BLOCK_DESCRIPTION, DEFAULT_PERSONA_BLOCK_DESCRIPTION
from letta.schemas.enums import PrimitiveType
from letta.schemas.letta_base import LettaBase
INT32_MAX = 2147483647
# block of the LLM context
class BaseBlock(LettaBase, validate_assignment=True):
"""Base block of the LLM context"""
__id_prefix__ = PrimitiveType.BLOCK.value
# data value
value: str = Field(..., description="Value of the block.")
limit: int = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, description="Character limit of the block.")
project_id: Optional[str] = Field(None, description="The associated project id.")
# template data (optional)
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
is_template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
template_id: Optional[str] = Field(None, description="The id of the template.")
base_template_id: Optional[str] = Field(None, description="The base template id of the block.")
deployment_id: Optional[str] = Field(None, description="The id of the deployment.")
entity_id: Optional[str] = Field(None, description="The id of the entity within the template.")
preserve_on_migration: Optional[bool] = Field(False, description="Preserve the block on template migration.")
# context window label
label: Optional[str] = Field(None, description="Label of the block (e.g. 'human', 'persona') in the context window.")
# permissions of the agent
read_only: bool = Field(False, description="Whether the agent has read-only access to the block.")
# metadata
description: Optional[str] = Field(None, description="Description of the block.")
metadata: Optional[dict] = Field({}, description="Metadata of the block.")
hidden: Optional[bool] = Field(
None,
description="If set to True, the block will be hidden.",
)
# def __len__(self):
# return len(self.value)
model_config = ConfigDict(extra="ignore") # Ignores extra fields
@field_validator("limit", mode="after")
@classmethod
def validate_limit_int32(cls, v: int) -> int:
"""Ensure limit is within PostgreSQL INTEGER (int32) range."""
if v > INT32_MAX:
raise ValueError(f"limit must be <= {INT32_MAX} (int32 max), got {v}")
return v
@field_validator("value", mode="before")
@classmethod
def sanitize_value_null_bytes(cls, v):
"""Remove null bytes from value to prevent PostgreSQL encoding errors."""
if isinstance(v, str):
return v.replace("\x00", "")
return v
@model_validator(mode="before")
@classmethod
def verify_char_limit(cls, data: Any) -> Any:
"""Validate the character limit before model instantiation.
Notes:
- Runs on raw input; do not mutate input.
- For update schemas (e.g., BlockUpdate), `value` and `limit` may be absent.
In that case, only validate when both are provided.
"""
if isinstance(data, dict):
limit = data.get("limit")
value = data.get("value")
# Only enforce the char limit when both are present.
# Pydantic will separately enforce required fields where applicable.
if limit is not None and value is not None and isinstance(value, str):
if len(value) > limit:
error_msg = f"Edit failed: Exceeds {limit} character limit (requested {len(value)})"
raise ValueError(error_msg)
return data
def __setattr__(self, name, value):
"""Run validation if self.value is updated"""
super().__setattr__(name, value)
if name == "value":
# run validation
self.__class__.model_validate(self.model_dump(exclude_unset=True))
class Block(BaseBlock):
"""A Block represents a reserved section of the LLM's context window."""
id: str = BaseBlock.generate_id_field()
# default orm fields
created_by_id: Optional[str] = Field(None, description="The id of the user that made this Block.")
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that last updated this Block.")
# tags - using Optional with default [] to allow None input to become empty list
tags: Optional[List[str]] = Field(default=[], description="The tags associated with the block.")
@model_validator(mode="before")
@classmethod
def normalize_tags(cls, data: dict) -> dict:
"""Convert None tags to empty list."""
if isinstance(data, dict) and data.get("tags") is None:
data["tags"] = []
return data
class BlockResponse(Block):
id: str = Field(
...,
description="The id of the block.",
)
template_name: Optional[str] = Field(
None, description="(Deprecated) The name of the block template (if it is a template).", deprecated=True
)
template_id: Optional[str] = Field(None, description="(Deprecated) The id of the template.", deprecated=True)
base_template_id: Optional[str] = Field(None, description="(Deprecated) The base template id of the block.", deprecated=True)
deployment_id: Optional[str] = Field(None, description="(Deprecated) The id of the deployment.", deprecated=True)
entity_id: Optional[str] = Field(None, description="(Deprecated) The id of the entity within the template.", deprecated=True)
preserve_on_migration: Optional[bool] = Field(
False, description="(Deprecated) Preserve the block on template migration.", deprecated=True
)
read_only: bool = Field(False, description="(Deprecated) Whether the agent has read-only access to the block.", deprecated=True)
hidden: Optional[bool] = Field(None, description="(Deprecated) If set to True, the block will be hidden.", deprecated=True)
class FileBlock(Block):
file_id: str = Field(..., description="Unique identifier of the file.")
source_id: str = Field(..., description="Deprecated: Use `folder_id` field instead. Unique identifier of the source.", deprecated=True)
is_open: bool = Field(..., description="True if the agent currently has the file open.")
last_accessed_at: Optional[datetime] = Field(
None,
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"""
label: str = "human"
description: Optional[str] = Field(DEFAULT_HUMAN_BLOCK_DESCRIPTION, description="Description of the block.")
class Persona(Block):
"""Persona block of the LLM context"""
label: str = "persona"
description: Optional[str] = Field(DEFAULT_PERSONA_BLOCK_DESCRIPTION, description="Description of the block.")
DEFAULT_BLOCKS = [Human(value=""), Persona(value="")]
class BlockUpdate(BaseBlock):
"""Update a block"""
limit: Optional[int] = Field(None, description="Character limit of the block.")
value: Optional[str] = Field(None, description="Value of the block.")
project_id: Optional[str] = Field(None, description="The associated project id.")
# tags
tags: Optional[List[str]] = Field(None, description="The tags to associate with the block.")
model_config = ConfigDict(extra="ignore") # Ignores extra fields
class CreateBlock(BaseBlock):
"""Create a block"""
label: str = Field(..., description="Label of the block.")
limit: int = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, description="Character limit of the block.")
value: str = Field(..., description="Value of the block.")
project_id: Optional[str] = Field(None, description="The associated project id.")
# block templates
is_template: bool = False
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
# tags
tags: Optional[List[str]] = Field(None, description="The tags to associate with the block.")
@model_validator(mode="before")
@classmethod
def ensure_value_is_string(cls, data):
"""Convert None value to empty string"""
if data and isinstance(data, dict) and data.get("value") is None:
data["value"] = ""
return data
class CreateHuman(CreateBlock):
"""Create a human block"""
label: str = "human"
class CreatePersona(CreateBlock):
"""Create a persona block"""
label: str = "persona"
class CreateBlockTemplate(CreateBlock):
"""Create a block template"""
is_template: bool = True
class CreateHumanBlockTemplate(CreateHuman):
"""Create a human block template"""
is_template: bool = True
label: str = "human"
class CreatePersonaBlockTemplate(CreatePersona):
"""Create a persona block template"""
is_template: bool = True
label: str = "persona"
class InternalTemplateBlockCreate(CreateBlock):
"""Used for Letta Cloud"""
base_template_id: str = Field(..., description="The id of the base template.")
template_id: str = Field(..., description="The id of the template.")
deployment_id: str = Field(..., description="The id of the deployment.")
entity_id: str = Field(..., description="The id of the entity within the template.")