feat: query param validation block label, name, and search (#6397)

* add block label, name, and search query param validation

* finishing touches on blocks

* remove default for blocks

* query changes to api spec

* openapi changes

* change descriptions
This commit is contained in:
Kian Jones
2025-11-25 16:20:04 -08:00
committed by Caren Thomas
parent 85604988bd
commit 3e92fecb03
4 changed files with 206 additions and 76 deletions

View File

@@ -10045,16 +10045,20 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 50,
"pattern": "^[a-zA-Z0-9_-]+$"
},
{
"type": "null"
}
],
"description": "Labels to include (e.g. human, persona)",
"description": "Label to include (alphanumeric, hyphens, underscores only)",
"examples": ["human", "persona", "the_label_of-a-block"],
"title": "Label"
},
"description": "Labels to include (e.g. human, persona)"
"description": "Label to include (alphanumeric, hyphens, underscores only)"
},
{
"name": "templates_only",
@@ -10075,16 +10079,20 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 100,
"pattern": "^[a-zA-Z0-9 _-]+$"
},
{
"type": "null"
}
],
"description": "Name of the block",
"description": "Name filter (alphanumeric, spaces, hyphens, underscores)",
"examples": ["My Agent", "test_tool", "default-config"],
"title": "Name"
},
"description": "Name of the block"
"description": "Name filter (alphanumeric, spaces, hyphens, underscores)"
},
{
"name": "identity_id",
@@ -10093,16 +10101,20 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 45,
"maxLength": 45,
"pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
},
{
"type": "null"
}
],
"description": "Search agents by identifier id",
"description": "The ID of the identity in the format 'identity-<uuid4>'",
"examples": ["identity-123e4567-e89b-42d3-8456-426614174000"],
"title": "Identity Id"
},
"description": "Search agents by identifier id"
"description": "The ID of the identity in the format 'identity-<uuid4>'"
},
{
"name": "identifier_keys",
@@ -10231,16 +10243,20 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 50,
"pattern": "^[a-zA-Z0-9_-]+$"
},
{
"type": "null"
}
],
"description": "Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels.",
"description": "Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels.",
"examples": ["human", "persona", "the_label_of-a-block"],
"title": "Label Search"
},
"description": "Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels."
"description": "Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels."
},
{
"name": "description_search",
@@ -10249,16 +10265,18 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 200
},
{
"type": "null"
}
],
"description": "Search blocks by description. If provided, returns blocks that match this description. This is a full-text search on block descriptions.",
"description": "Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions.",
"title": "Description Search"
},
"description": "Search blocks by description. If provided, returns blocks that match this description. This is a full-text search on block descriptions."
"description": "Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions."
},
{
"name": "value_search",
@@ -10267,16 +10285,18 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 200
},
{
"type": "null"
}
],
"description": "Search blocks by value. If provided, returns blocks that match this value.",
"description": "Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values.",
"title": "Value Search"
},
"description": "Search blocks by value. If provided, returns blocks that match this value."
"description": "Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values."
},
{
"name": "connected_to_agents_count_gt",
@@ -11939,16 +11959,20 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 50,
"pattern": "^[a-zA-Z0-9_-]+$"
},
{
"type": "null"
}
],
"description": "Labels to include (e.g. human, persona)",
"description": "Label to include (alphanumeric, hyphens, underscores only)",
"examples": ["human", "persona", "the_label_of-a-block"],
"title": "Label"
},
"description": "Labels to include (e.g. human, persona)"
"description": "Label to include (alphanumeric, hyphens, underscores only)"
},
{
"name": "templates_only",
@@ -11969,16 +11993,20 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 100,
"pattern": "^[a-zA-Z0-9 _-]+$"
},
{
"type": "null"
}
],
"description": "Name of the block",
"description": "Name filter (alphanumeric, spaces, hyphens, underscores)",
"examples": ["My Agent", "test_tool", "default-config"],
"title": "Name"
},
"description": "Name of the block"
"description": "Name filter (alphanumeric, spaces, hyphens, underscores)"
},
{
"name": "identity_id",
@@ -11987,16 +12015,20 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 45,
"maxLength": 45,
"pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
},
{
"type": "null"
}
],
"description": "Search agents by identifier id",
"description": "The ID of the identity in the format 'identity-<uuid4>'",
"examples": ["identity-123e4567-e89b-42d3-8456-426614174000"],
"title": "Identity Id"
},
"description": "Search agents by identifier id"
"description": "The ID of the identity in the format 'identity-<uuid4>'"
},
{
"name": "identifier_keys",
@@ -12125,16 +12157,20 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 50,
"pattern": "^[a-zA-Z0-9_-]+$"
},
{
"type": "null"
}
],
"description": "Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels.",
"description": "Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels.",
"examples": ["human", "persona", "the_label_of-a-block"],
"title": "Label Search"
},
"description": "Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels."
"description": "Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels."
},
{
"name": "description_search",
@@ -12143,16 +12179,18 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 200
},
{
"type": "null"
}
],
"description": "Search blocks by description. If provided, returns blocks that match this description. This is a full-text search on block descriptions.",
"description": "Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions.",
"title": "Description Search"
},
"description": "Search blocks by description. If provided, returns blocks that match this description. This is a full-text search on block descriptions."
"description": "Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions."
},
{
"name": "value_search",
@@ -12161,16 +12199,18 @@
"schema": {
"anyOf": [
{
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 200
},
{
"type": "null"
}
],
"description": "Search blocks by value. If provided, returns blocks that match this value.",
"description": "Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values.",
"title": "Value Search"
},
"description": "Search blocks by value. If provided, returns blocks that match this value."
"description": "Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values."
},
{
"name": "connected_to_agents_count_gt",

View File

@@ -8,7 +8,15 @@ from letta.schemas.block import BaseBlock, Block, BlockResponse, BlockUpdate, Cr
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
from letta.server.server import SyncServer
from letta.utils import is_1_0_sdk_version
from letta.validators import BlockId
from letta.validators import (
BlockDescriptionSearchQuery,
BlockId,
BlockLabelQuery,
BlockLabelSearchQuery,
BlockNameQuery,
BlockValueSearchQuery,
IdentityIdQuery,
)
if TYPE_CHECKING:
pass
@@ -19,10 +27,10 @@ router = APIRouter(prefix="/blocks", tags=["blocks"])
@router.get("/", response_model=List[BlockResponse], operation_id="list_blocks")
async def list_blocks(
# query parameters
label: Optional[str] = Query(None, description="Labels to include (e.g. human, persona)"),
label: BlockLabelQuery = None,
templates_only: bool = Query(False, description="Whether to include only templates"),
name: Optional[str] = Query(None, description="Name of the block"),
identity_id: Optional[str] = Query(None, description="Search agents by identifier id"),
name: BlockNameQuery = None,
identity_id: IdentityIdQuery = None,
identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"),
project_id: Optional[str] = Query(None, description="Search blocks by project id"),
limit: Optional[int] = Query(50, description="Number of blocks to return"),
@@ -38,21 +46,9 @@ async def list_blocks(
"asc", description="Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first"
),
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
label_search: Optional[str] = Query(
None,
description=("Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels."),
),
description_search: Optional[str] = Query(
None,
description=(
"Search blocks by description. If provided, returns blocks that match this description. "
"This is a full-text search on block descriptions."
),
),
value_search: Optional[str] = Query(
None,
description=("Search blocks by value. If provided, returns blocks that match this value."),
),
label_search: BlockLabelSearchQuery = None,
description_search: BlockDescriptionSearchQuery = None,
value_search: BlockValueSearchQuery = None,
connected_to_agents_count_gt: Optional[int] = Query(
None,
description=(

View File

@@ -7,7 +7,15 @@ from letta.schemas.block import Block, CreateBlock
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
from letta.server.server import SyncServer
from letta.utils import is_1_0_sdk_version
from letta.validators import BlockId
from letta.validators import (
BlockDescriptionSearchQuery,
BlockId,
BlockLabelQuery,
BlockLabelSearchQuery,
BlockNameQuery,
BlockValueSearchQuery,
IdentityIdQuery,
)
if TYPE_CHECKING:
pass
@@ -18,10 +26,10 @@ router = APIRouter(prefix="/_internal_blocks", tags=["_internal_blocks"])
@router.get("/", response_model=List[Block], operation_id="list_internal_blocks")
async def list_blocks(
# query parameters
label: Optional[str] = Query(None, description="Labels to include (e.g. human, persona)"),
label: BlockLabelQuery = None,
templates_only: bool = Query(False, description="Whether to include only templates"),
name: Optional[str] = Query(None, description="Name of the block"),
identity_id: Optional[str] = Query(None, description="Search agents by identifier id"),
name: BlockNameQuery = None,
identity_id: IdentityIdQuery = None,
identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"),
project_id: Optional[str] = Query(None, description="Search blocks by project id"),
limit: Optional[int] = Query(50, description="Number of blocks to return"),
@@ -37,21 +45,9 @@ async def list_blocks(
"asc", description="Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first"
),
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
label_search: Optional[str] = Query(
None,
description=("Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels."),
),
description_search: Optional[str] = Query(
None,
description=(
"Search blocks by description. If provided, returns blocks that match this description. "
"This is a full-text search on block descriptions."
),
),
value_search: Optional[str] = Query(
None,
description=("Search blocks by value. If provided, returns blocks that match this value."),
),
label_search: BlockLabelSearchQuery = None,
description_search: BlockDescriptionSearchQuery = None,
value_search: BlockValueSearchQuery = None,
connected_to_agents_count_gt: Optional[int] = Query(
None,
description=(

View File

@@ -1,9 +1,9 @@
import inspect
import re
from functools import wraps
from typing import Annotated
from typing import Annotated, Optional
from fastapi import Path
from fastapi import Path, Query
from letta.errors import LettaInvalidArgumentError
from letta.schemas.enums import PrimitiveType # PrimitiveType is now in schemas.enums
@@ -124,3 +124,101 @@ def raise_on_invalid_id(param_name: str, expected_prefix: PrimitiveType):
return wrapper
return decorator
# =============================================================================
# Query Parameter Validators
# =============================================================================
# Format validators for common query parameters to match frontend constraints
def _create_id_query_validator(primitive: str):
"""
Creates a Query validator for ID parameters with format validation.
Args:
primitive: The primitive type prefix (e.g., "agent", "tool")
Returns:
A Query validator with pattern matching
"""
return Query(
description=f"The ID of the {primitive} in the format '{primitive}-<uuid4>'",
pattern=PRIMITIVE_ID_PATTERNS[primitive].pattern,
examples=[f"{primitive}-123e4567-e89b-42d3-8456-426614174000"],
min_length=len(primitive) + 1 + 36,
max_length=len(primitive) + 1 + 36,
)
# Query parameter ID validators with format checking
AgentIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.AGENT.value)]
ToolIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.TOOL.value)]
SourceIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.SOURCE.value)]
BlockIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.BLOCK.value)]
MessageIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.MESSAGE.value)]
RunIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.RUN.value)]
JobIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.JOB.value)]
GroupIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.GROUP.value)]
IdentityIdQuery = Annotated[Optional[str], _create_id_query_validator(PrimitiveType.IDENTITY.value)]
# =============================================================================
# String Field Validators
# =============================================================================
# Format validators for common string fields
# Label validator: alphanumeric, hyphens, underscores, max 50 chars
BlockLabelQuery = Annotated[
Optional[str],
Query(
description="Label to include (alphanumeric, hyphens, underscores only)",
pattern=r"^[a-zA-Z0-9_-]+$",
min_length=1,
max_length=50,
examples=["human", "persona", "the_label_of-a-block"],
),
]
# Name validator: similar to label but allows spaces, max 100 chars
BlockNameQuery = Annotated[
Optional[str],
Query(
description="Name filter (alphanumeric, spaces, hyphens, underscores)",
pattern=r"^[a-zA-Z0-9 _-]+$",
min_length=1,
max_length=100,
examples=["My Agent", "test_tool", "default-config"],
),
]
# Search query validator: general text search, max 200 chars
BlockLabelSearchQuery = Annotated[
Optional[str],
Query(
description="Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels.",
pattern=r"^[a-zA-Z0-9_-]+$",
min_length=1,
max_length=50,
examples=["human", "persona", "the_label_of-a-block"],
),
]
BlockValueSearchQuery = Annotated[
Optional[str],
Query(
description="Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values.",
min_length=1,
max_length=200,
),
]
BlockDescriptionSearchQuery = Annotated[
Optional[str],
Query(
description="Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions.",
min_length=1,
max_length=200,
),
]