Skip to content

Commit 564ee3f

Browse files
committed
Update chanelogs and remove dict output type
1 parent 8ea1936 commit 564ee3f

7 files changed

Lines changed: 96 additions & 8 deletions

File tree

providers/common/ai/docs/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ and :meth:`~airflow.providers.common.ai.hooks.pydantic_ai.PydanticAIHook.run_age
5353
DAG authors using :class:`~airflow.providers.common.ai.operators.agent.AgentOperator`,
5454
``@task.agent``, and the other LLM operators are unaffected.
5555

56+
``SQLToolset`` now implements the framework-neutral
57+
:class:`~airflow.providers.common.ai.hooks.base.BaseToolset` interface instead of
58+
pydantic-ai's ``AbstractToolset`` interface. DAG authors using ``SQLToolset``
59+
with ``AgentOperator`` or ``@task.agent`` are unaffected. Direct pydantic-ai
60+
``Agent(toolsets=[SQLToolset(...)])`` callers should use
61+
``AgentOperator(toolsets=[SQLToolset(...)])`` or pass the SQL tool callables
62+
through an Airflow agent hook request.
63+
5664
0.4.0
5765
.....
5866

providers/common/ai/docs/operators/agent.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,11 @@ Parameters
312312
(``SQLToolset``), pydantic-ai ``AbstractToolset`` implementations
313313
(``HookToolset``, ``MCPToolset``, ``DataFusionToolset``,
314314
``AgentSkillsToolset`` for :ref:`agent-skills`, third-party toolsets),
315-
plain Python callables, or native pydantic-ai ``Tool`` objects. Mixed lists
316-
are supported.
315+
pydantic-ai ``DynamicToolset`` instances, plain Python callables, or native
316+
pydantic-ai ``Tool`` objects. Mixed lists are supported. Bare Python
317+
callables are treated as callable tools; wrap pydantic-ai ``ToolsetFunc``
318+
factories with ``DynamicToolset`` to pass them through as native dynamic
319+
toolsets.
317320
- ``enable_tool_logging``: When ``True`` (default), wraps each tool call with
318321
real-time logging. For pydantic-ai ``AbstractToolset`` items this is done via
319322
:class:`~airflow.providers.common.ai.toolsets.logging.LoggingToolset`; for

providers/common/ai/docs/toolsets.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,35 @@ toolset to the correct agent parameter automatically.
5353
- Any third-party ``AbstractToolset``, including PydanticAI's own MCP
5454
server classes (``MCPServerStreamableHTTP``, ``MCPServerSSE``,
5555
``MCPServerStdio``).
56+
- pydantic-ai dynamic toolsets, by wrapping a ``ToolsetFunc`` factory with
57+
``DynamicToolset``.
5658
- :class:`~airflow.providers.common.ai.hooks.base.BaseToolset`
5759
subclasses (``SQLToolset``).
5860
- Plain Python callables (``def my_tool(...): ...``).
5961
- Native pydantic-ai ``Tool`` objects.
6062

6163
The hook routes each item to the correct agent parameter automatically.
64+
Bare Python callables are treated as callable tools. To pass a pydantic-ai
65+
``ToolsetFunc`` factory through as a native dynamic toolset, wrap it with
66+
``DynamicToolset``:
67+
68+
.. code-block:: python
69+
70+
from pydantic_ai import RunContext
71+
from pydantic_ai.agent import DynamicToolset
72+
from pydantic_ai.toolsets import AbstractToolset
73+
74+
75+
def select_toolset(ctx: RunContext) -> AbstractToolset | None:
76+
return None
77+
78+
79+
AgentOperator(
80+
task_id="agent",
81+
prompt="Answer with the tools available for this run.",
82+
llm_conn_id="pydanticai_default",
83+
toolsets=[DynamicToolset(select_toolset)],
84+
)
6285
6386
6487
Using Toolsets Directly

providers/common/ai/src/airflow/providers/common/ai/hooks/base.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ class AgentRunRequest:
137137
138138
:param prompt: User prompt for this invocation (plain ``str`` or a multimodal
139139
``Sequence`` accepted by the backend agent's run API).
140-
:param output_type: Expected structured output type or backend-specific JSON schema
141-
mapping (default: ``str``).
140+
:param output_type: Expected structured output type (default: ``str``).
142141
:param instructions: System-level instructions for the agent.
143142
:param toolsets: List of tools/toolsets the agent may call (BaseToolset instances, plain callables, or backend-native tool objects).
144143
:param usage_limits: Backend-specific usage limits; ignored if the hook does not support them.
@@ -152,7 +151,7 @@ class AgentRunRequest:
152151
"""
153152

154153
prompt: str | Sequence[Any]
155-
output_type: type[Any] | dict[str, Any] | None = str
154+
output_type: type[Any] | None = str
156155
instructions: str = ""
157156
toolsets: list[Any] | None = None
158157
usage_limits: Any = None

providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,12 @@ def _build_agent(self, request: AgentRunRequest) -> PydanticAgentHandle:
298298
[type(toolset).__name__ for toolset in processed],
299299
)
300300

301+
if isinstance(request.output_type, dict):
302+
raise ValueError(
303+
"PydanticAIHook does not support raw JSON schema mappings for output_type. "
304+
"Pass a Python type, such as a Pydantic BaseModel subclass."
305+
)
306+
301307
agent_kwargs: dict[str, Any] = {"instructions": request.instructions, **extra_kwargs}
302308
if request.output_type is not None:
303309
agent_kwargs["output_type"] = request.output_type

providers/common/ai/src/airflow/providers/common/ai/toolsets/langchain_bridge.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from pydantic_ai.usage import RunUsage
4242

4343
from airflow.providers.common.ai.hooks.base import BaseToolset
44+
from airflow.providers.common.ai.utils.callables import is_async_callable
4445

4546
if TYPE_CHECKING:
4647
from collections.abc import Coroutine
@@ -187,15 +188,15 @@ def _build_structured_tool_from_spec(
187188

188189
def _sync_call(**kwargs: Any) -> Any:
189190
try:
190-
if asyncio.iscoroutinefunction(spec.fn):
191+
if is_async_callable(spec.fn):
191192
return _run_coro_sync(spec.fn(**kwargs))
192193
return spec.fn(**kwargs)
193194
except ModelRetry as e:
194195
return str(e)
195196

196197
async def _async_call(**kwargs: Any) -> Any:
197198
try:
198-
if asyncio.iscoroutinefunction(spec.fn):
199+
if is_async_callable(spec.fn):
199200
return await spec.fn(**kwargs)
200201
return spec.fn(**kwargs)
201202
except ModelRetry as e:

providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
import functools
2020
import json
2121
import sys
22+
from typing import TYPE_CHECKING
2223
from unittest.mock import MagicMock, patch
2324

2425
import pytest
25-
from pydantic_ai import Agent
26+
from pydantic_ai import Agent, RunContext
27+
from pydantic_ai.agent import DynamicToolset
2628
from pydantic_ai.messages import ModelResponse, TextPart
2729
from pydantic_ai.models import Model
2830
from pydantic_ai.models.test import TestModel
@@ -46,6 +48,9 @@
4648
)
4749
from airflow.providers.common.ai.mixins.durable import DurableState
4850

51+
if TYPE_CHECKING:
52+
from pydantic_ai.toolsets import AbstractToolset
53+
4954

5055
def _test_agent() -> Agent[None, str]:
5156
return Agent(TestModel())
@@ -453,6 +458,25 @@ def test_create_agent_with_agent_params(self, mock_agent_cls, mock_infer_model):
453458
retries=3,
454459
)
455460

461+
@patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_model", autospec=True)
462+
@patch("airflow.providers.common.ai.hooks.pydantic_ai.Agent", autospec=True)
463+
def test_create_agent_rejects_raw_json_schema_output_type(self, mock_agent_cls, mock_infer_model):
464+
mock_model = MagicMock(spec=Model)
465+
mock_infer_model.return_value = mock_model
466+
467+
hook = PydanticAIHook(llm_conn_id="test_conn", model_id="openai:gpt-5.3")
468+
conn = Connection(conn_id="test_conn", conn_type="pydanticai")
469+
request = AgentRunRequest(
470+
prompt="hi",
471+
output_type={"type": "object", "properties": {}}, # type: ignore[arg-type]
472+
)
473+
474+
with patch.object(hook, "get_connection", return_value=conn):
475+
with pytest.raises(ValueError, match="raw JSON schema mappings"):
476+
hook.create_agent(request)
477+
478+
mock_agent_cls.assert_not_called()
479+
456480
@patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_model", autospec=True)
457481
@patch("airflow.providers.common.ai.hooks.pydantic_ai.Agent", autospec=True)
458482
def test_create_agent_rejects_tools_in_agent_params_with_toolsets(self, mock_agent_cls, mock_infer_model):
@@ -650,6 +674,30 @@ def test_create_agent_routes_abstract_toolset_to_toolsets_kwarg(self, mock_agent
650674
assert "toolsets" in call_kwargs
651675
assert any(ts is abstract_ts for ts in call_kwargs["toolsets"])
652676

677+
@patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_model", autospec=True)
678+
@patch("airflow.providers.common.ai.hooks.pydantic_ai.Agent", autospec=True)
679+
def test_create_agent_routes_dynamic_toolset_to_toolsets_kwarg(self, mock_agent_cls, mock_infer_model):
680+
"""DynamicToolset-wrapped factories must pass through as native pydantic-ai toolsets."""
681+
682+
mock_model = MagicMock(spec=Model)
683+
mock_infer_model.return_value = mock_model
684+
685+
def select_toolset(ctx: RunContext) -> AbstractToolset | None:
686+
return None
687+
688+
dynamic_toolset = DynamicToolset(select_toolset)
689+
690+
hook = PydanticAIHook(llm_conn_id="test_conn", model_id="openai:gpt-5.3")
691+
conn = Connection(conn_id="test_conn", conn_type="pydanticai")
692+
request = AgentRunRequest(prompt="hi", toolsets=[dynamic_toolset], enable_tool_logging=False)
693+
with patch.object(hook, "get_connection", return_value=conn):
694+
hook.create_agent(request)
695+
696+
call_kwargs = mock_agent_cls.call_args[1]
697+
assert "tools" not in call_kwargs
698+
assert "toolsets" in call_kwargs
699+
assert any(ts is dynamic_toolset for ts in call_kwargs["toolsets"])
700+
653701
@patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_model", autospec=True)
654702
@patch("airflow.providers.common.ai.hooks.pydantic_ai.Agent", autospec=True)
655703
def test_create_agent_wraps_abstract_toolset_with_logging(self, mock_agent_cls, mock_infer_model):

0 commit comments

Comments
 (0)