diff --git a/haystack/tools/from_function.py b/haystack/tools/from_function.py index 93a5d99c5ce..25969cb171a 100644 --- a/haystack/tools/from_function.py +++ b/haystack/tools/from_function.py @@ -68,7 +68,8 @@ def get_weather( The function must include type hints for all parameters. The function is expected to have basic python input types (str, int, float, bool, list, dict, tuple). Other input types may work but are not guaranteed. - If a parameter is annotated using `typing.Annotated`, its metadata will be used as parameter description. + If a parameter is annotated using `typing.Annotated`, the first string in its metadata will be used as + parameter description. Other metadata, such as a Pydantic `Field`, is passed on to Pydantic. :param name: The name of the Tool. If not provided, the name of the function will be used. :param description: @@ -164,8 +165,11 @@ def get_weather( default = param.default if param.default is not param.empty else ... fields[param_name] = (annotation, default) + # Only a string in `Annotated` metadata is a description; other metadata like `Field(...)` is left to Pydantic if hasattr(annotation, "__metadata__"): - descriptions[param_name] = annotation.__metadata__[0] + description = next((meta for meta in annotation.__metadata__ if isinstance(meta, str)), None) + if description is not None: + descriptions[param_name] = description # create Pydantic model and generate JSON schema try: diff --git a/releasenotes/notes/tool-from-function-annotated-pydantic-field-77c195c002682e00.yaml b/releasenotes/notes/tool-from-function-annotated-pydantic-field-77c195c002682e00.yaml new file mode 100644 index 00000000000..8aef11aa12b --- /dev/null +++ b/releasenotes/notes/tool-from-function-annotated-pydantic-field-77c195c002682e00.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + ``create_tool_from_function`` and the ``@tool`` decorator no longer fail with + "The provided parameters do not define a valid JSON schema" when a parameter is annotated with a Pydantic + ``Field``, for example ``Annotated[int, Field(ge=1)]``. Previously, the first ``Annotated`` metadata item was + always written to the schema as the parameter description, even when it was not a string. + Now only a string metadata item is used as the description, and a ``Field`` is left to Pydantic, so its + constraints and its own ``description`` appear in the tool schema. diff --git a/test/tools/test_from_function.py b/test/tools/test_from_function.py index 2592d2999a9..1f3621e7766 100644 --- a/test/tools/test_from_function.py +++ b/test/tools/test_from_function.py @@ -128,6 +128,28 @@ def function_with_postponed_annotations( } +def test_from_function_annotated_with_pydantic_field(): + def function_with_fields( + city: Annotated[str, Field(min_length=1), "the city for which to get the weather"], + days: Annotated[int, Field(ge=1, description="the number of days to forecast")] = 1, + verbose: Annotated[bool, Field(strict=True)] = False, + ) -> str: + """A simple function to get the weather forecast for a location.""" + return f"Weather report for {city}: 20°C, sunny" + + tool = create_tool_from_function(function=function_with_fields) + + assert tool.parameters == { + "type": "object", + "properties": { + "city": {"type": "string", "minLength": 1, "description": "the city for which to get the weather"}, + "days": {"type": "integer", "minimum": 1, "description": "the number of days to forecast", "default": 1}, + "verbose": {"type": "boolean", "default": False}, + }, + "required": ["city"], + } + + def test_from_function_missing_type_hint(): def function_missing_type_hint(city) -> str: # type: ignore[no-untyped-def] return f"Weather report for {city}: 20°C, sunny"