Skip to content

Commit 82f087c

Browse files
committed
Update functions schema
1 parent 2c18ae1 commit 82f087c

2 files changed

Lines changed: 35 additions & 11 deletions

File tree

providers/common/ai/src/airflow/providers/common/ai/utils/function_schema.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
)
5454

5555

56-
def _first_docstring_paragraph(obj: Any) -> str:
56+
def _extract_docstring_summary(obj: Any) -> str:
57+
"""Return leading descriptive docstring text before Args/Returns-style sections."""
5758
doc = inspect.getdoc(obj)
5859
if not doc:
5960
return ""
@@ -66,7 +67,7 @@ def _first_docstring_paragraph(obj: Any) -> str:
6667

6768

6869
def extract_function_description(fn: Callable[..., Any]) -> str:
69-
"""Return the first paragraph of *fn*'s docstring, stopping before Args/Returns sections."""
70+
"""Return a short description for *fn* from its leading docstring text."""
7071
# Unwrap partials to get the underlying function's docstring.
7172
if isinstance(fn, functools.partial):
7273
return extract_function_description(fn.func)
@@ -75,12 +76,12 @@ def extract_function_description(fn: Callable[..., Any]) -> str:
7576
# Prefer __call__ docstring (what calling does), then class docstring, then class name.
7677
if not hasattr(fn, "__name__"):
7778
return (
78-
_first_docstring_paragraph(type(fn).__call__)
79-
or _first_docstring_paragraph(fn)
79+
_extract_docstring_summary(type(fn).__call__)
80+
or _extract_docstring_summary(fn)
8081
or type(fn).__name__
8182
)
8283

83-
return _first_docstring_paragraph(fn) or fn.__name__
84+
return _extract_docstring_summary(fn) or fn.__name__
8485

8586

8687
def build_function_json_schema(fn: Callable[..., Any]) -> dict[str, Any]:
@@ -92,11 +93,10 @@ def build_function_json_schema(fn: Callable[..., Any]) -> dict[str, Any]:
9293
Falls back to an empty object schema on any introspection failure.
9394
9495
``self``, ``cls``, ``*args``, and ``**kwargs`` are excluded.
96+
Positional-only params are rejected because tool callables must accept
97+
keyword arguments matching the generated schema.
9598
For ``functools.partial``, only the remaining free parameters appear.
9699
"""
97-
if inspect.isbuiltin(fn):
98-
return _EMPTY_OBJECT_SCHEMA
99-
100100
# Partials: sig from partial (bound args already removed), hints from inner fn.
101101
hint_source: Callable[..., Any] = fn
102102
if isinstance(fn, functools.partial):
@@ -118,6 +118,17 @@ def build_function_json_schema(fn: Callable[..., Any]) -> dict[str, Any]:
118118
for param_name, param in sig.parameters.items():
119119
if param_name in _SKIP_PARAMS:
120120
continue
121+
if param.kind is inspect.Parameter.POSITIONAL_ONLY:
122+
# Auto-generated tool schemas describe named JSON object fields,
123+
# and tool frameworks invoke the callable with keyword arguments
124+
# derived from those fields. A positional-only parameter cannot be
125+
# satisfied by that contract, so fail fast with a clear error.
126+
name = getattr(fn, "__name__", type(fn).__name__)
127+
raise ValueError(
128+
f"Cannot build a tool schema for {name}: "
129+
f"parameter {param_name!r} is positional-only. "
130+
"Tool parameters must be callable by keyword."
131+
)
121132
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
122133
continue
123134

providers/common/ai/tests/unit/common/ai/utils/test_function_schema.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from __future__ import annotations
1818

1919
import functools
20+
import inspect
2021
from typing import Annotated, Any
2122

2223
import pytest
@@ -250,6 +251,12 @@ def fn(**kwargs: str): ...
250251
schema = build_function_json_schema(fn)
251252
assert "kwargs" not in schema.get("properties", {})
252253

254+
def test_positional_only_rejected(self):
255+
def fn(x: int, /, y: str): ...
256+
257+
with pytest.raises(ValueError, match="parameter 'x' is positional-only"):
258+
build_function_json_schema(fn)
259+
253260
def test_unannotated_param_included_as_any(self):
254261
def fn(x): ...
255262

@@ -301,9 +308,15 @@ def fn(x: int | None = None): ...
301308
assert "x" in schema["properties"]
302309
assert "x" not in schema.get("required", [])
303310

304-
def test_introspection_failure_returns_empty_schema(self):
305-
# Built-in functions have no inspectable signature.
306-
schema = build_function_json_schema(len)
311+
def test_signature_failure_returns_empty_schema(self, monkeypatch):
312+
def fn(x: int): ...
313+
314+
def raise_value_error(_):
315+
raise ValueError("boom")
316+
317+
monkeypatch.setattr(inspect, "signature", raise_value_error)
318+
319+
schema = build_function_json_schema(fn)
307320
assert schema == _EMPTY_OBJECT_SCHEMA
308321

309322
def test_callable_object_schema_from_call(self):

0 commit comments

Comments
 (0)