Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion livekit-agents/livekit/agents/llm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,13 @@ def function_arguments_to_pydantic_model(func: Callable[..., Any]) -> type[BaseM
)
if annotated_field and hasattr(annotated_field, "asdict"):
# `asdict` is available after pydantic 2.12
field_attrs = annotated_field.asdict()["attributes"]
field_dict = annotated_field.asdict()
field_attrs = field_dict["attributes"]
# Constraints (ge/le/gt/lt/multiple_of/min_length/pattern/...) live
# in `metadata`, not `attributes`. Re-attach them to the annotation
# so `Field(...)` constraints on a tool argument are preserved.
if field_dict["metadata"]:
type_hint = Annotated[(type_hint, *field_dict["metadata"])]
elif annotated_field:
field_attrs["default"] = annotated_field.default
field_attrs["description"] = annotated_field.description
Expand Down
22 changes: 21 additions & 1 deletion tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Annotated, Any, Literal

import pytest
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ValidationError

from livekit.agents import Agent
from livekit.agents.llm import ProviderTool, Tool, ToolContext, ToolError, Toolset, function_tool
Expand Down Expand Up @@ -338,6 +338,26 @@ def test_function_arguments_to_pydantic_model(self):
"type": "object",
}

def test_field_constraints_preserved(self):
# Field(...) constraints (ge/le/pattern/...) live in FieldInfo.metadata,
# not its attributes; they must survive the signature -> pydantic model
# conversion so the model both advertises and enforces them.
@function_tool
async def book(count: Annotated[int, Field(ge=1, le=10, description="how many")]) -> str:
"""Book a thing."""
return "ok"

model = function_arguments_to_pydantic_model(book)
prop = model.model_json_schema()["properties"]["count"]
assert prop["minimum"] == 1
assert prop["maximum"] == 10
assert prop["description"] == "how many"

model(count=5) # within bounds
for bad in (0, 11):
with pytest.raises(ValidationError):
model(count=bad)

async def test_tool_execution(self):
args, kwargs = prepare_function_arguments(
fnc=mock_tool_1, json_arguments='{"arg1": "test", "opt_arg2": "test2"}'
Expand Down
Loading