From 41e7fb53dcf84c0f731f8eb265e549cc31fa7f73 Mon Sep 17 00:00:00 2001 From: Saquib Jawed <138011435+saquibjawedbit@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:21:14 +0530 Subject: [PATCH 1/2] Python: Keep Field constraints after an Annotated tool description _parse_annotation wrapped the metadata that follows a string description in a single tuple, so Pydantic ignored it. Constraints such as Field(ge=1, le=10) were missing from the tool schema and not enforced when the tool was invoked. Unpack the remaining metadata instead. Fixes #8643 --- .../packages/core/agent_framework/_tools.py | 2 +- python/packages/core/tests/core/test_tools.py | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index bfde7930c10..f75588e20b7 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -1471,7 +1471,7 @@ def _parse_annotation(annotation: Any) -> Any: args_list = list(args) if len(args_list) == 2: return Annotated[args_list[0], Field(description=args_list[1])] - return Annotated[args_list[0], Field(description=args_list[1]), tuple(args_list[2:])] + return Annotated[(args_list[0], Field(description=args_list[1]), *args_list[2:])] return annotation diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index 3071e191513..9269af4fc46 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1565,6 +1565,43 @@ def test_parse_annotation_with_annotated_and_literal(): assert get_args(literal_type) == ("A", "B", "C") +def test_parse_annotation_with_description_preserves_extra_metadata(): + """Test that metadata after a string description is kept as separate Annotated metadata.""" + from pydantic import Field + + constraints = Field(ge=1, le=10) + result = _parse_annotation(Annotated[int, "How many items", constraints]) + + assert get_origin(result) is Annotated + args = get_args(result) + assert args[0] is int + assert args[1].description == "How many items" + assert args[2] is constraints + + +async def test_tool_annotated_description_keeps_field_constraints(): + """Test that Field constraints after a string description reach the schema and validation.""" + from pydantic import Field + + @tool + def pick(count: Annotated[int, "How many items", Field(ge=1, le=10)]) -> str: + return str(count) + + assert pick.parameters()["properties"]["count"] == { + "description": "How many items", + "maximum": 10, + "minimum": 1, + "title": "Count", + "type": "integer", + } + + result = await pick.invoke(arguments={"count": 5}) + assert result[0].text == "5" + + with pytest.raises(TypeError, match="less than or equal to 10"): + await pick.invoke(arguments={"count": 999}) + + # endregion From 6b2a4b39dbd818a7a8f5bb81f43cbe9e6ac5481a Mon Sep 17 00:00:00 2001 From: Saquib Jawed <138011435+saquibjawedbit@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:30:02 +0530 Subject: [PATCH 2/2] Python: Assert only the relevant schema keys and Pydantic error type in the Annotated constraint test --- python/packages/core/tests/core/test_tools.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index 9269af4fc46..c20c72a3148 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1587,18 +1587,15 @@ async def test_tool_annotated_description_keeps_field_constraints(): def pick(count: Annotated[int, "How many items", Field(ge=1, le=10)]) -> str: return str(count) - assert pick.parameters()["properties"]["count"] == { - "description": "How many items", - "maximum": 10, - "minimum": 1, - "title": "Count", - "type": "integer", - } + count_schema = pick.parameters()["properties"]["count"] + assert count_schema["description"] == "How many items" + assert count_schema["minimum"] == 1 + assert count_schema["maximum"] == 10 result = await pick.invoke(arguments={"count": 5}) assert result[0].text == "5" - with pytest.raises(TypeError, match="less than or equal to 10"): + with pytest.raises(TypeError, match="less_than_equal"): await pick.invoke(arguments={"count": 999})