Skip to content

Commit e635df1

Browse files
committed
Add BaseAIHook and Update usage
1 parent 325f377 commit e635df1

35 files changed

Lines changed: 2174 additions & 1179 deletions

providers/common/ai/AGENTS.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ The hook is a thin bridge between Airflow connections and pydantic-ai's model/pr
1212
Bedrock, Ollama, etc.) via `infer_model()` and provider classes like `AzureProvider`, `BedrockProvider`.
1313
Do not re-implement provider-specific logic that pydantic-ai handles.
1414
Before writing new code, check: https://ai.pydantic.dev/models/
15-
- **Keep the hook thin.** `PydanticAIHook.get_conn()` maps Airflow connection fields to pydantic-ai
16-
constructors. That is the hook's entire job. Do not add abstraction layers (builders, factories,
17-
registries, Protocols) on top of pydantic-ai's own abstractions.
18-
- **No premature abstraction.** Do not add Protocols, builder patterns, or plugin systems for a single
19-
code path. Wait until there are 3+ concrete use cases before introducing an abstraction.
15+
- **Keep LLM hooks thin.** `PydanticAIHook.get_conn()` maps Airflow connection fields to pydantic-ai
16+
constructors. That is the hook's entire job for one-shot LLM operators.
17+
- **Agent backends use `BaseAIHook`.** `AgentOperator` / `@task.agent` resolve
18+
`BaseAIHook.get_agent_hook(conn_id)` so the connection ``conn_type`` selects the runtime
19+
(``pydanticai``, ``pydanticai-bedrock``, ``pydanticai-azure``, …). New agent frameworks subclass
20+
`BaseAIHook` and implement `get_model`, `create_agent`, `run_agent`, and `_tool_spec_to_native`;
21+
do not add parallel operator classes per framework.
2022
- **Operators stay focused.** Each operator does one thing: `LLMOperator` (prompt → output),
2123
`LLMBranchOperator` (prompt → branch decision), `LLMSQLOperator` (prompt → validated SQL).
2224
- **One backend per toolset.** A toolset wraps a single execution backend (e.g. `DbApiHook`,
@@ -67,7 +69,8 @@ building a wrapper here.
6769

6870
## Key Paths
6971

70-
- Hook: `src/airflow/providers/common/ai/hooks/pydantic_ai.py`
72+
- Hooks: `src/airflow/providers/common/ai/hooks/pydantic_ai.py` (pydantic-ai)
73+
- Base hook contract: `src/airflow/providers/common/ai/hooks/base_ai.py`
7174
- Operators: `src/airflow/providers/common/ai/operators/`
7275
- Decorators: `src/airflow/providers/common/ai/decorators/`
7376
- Toolsets: `src/airflow/providers/common/ai/toolsets/`

providers/common/ai/docs/changelog.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
Changelog
2626
---------
2727

28+
Next release
29+
............
30+
31+
Features
32+
^^^^^^^^
33+
34+
* Add ``BaseAIHook`` contract with framework-agnostic ``create_agent`` / ``run_agent`` /
35+
``get_model`` interface so ``AgentOperator`` selects the agent backend via connection type.
36+
2837
0.3.0
2938
.....
3039

providers/common/ai/docs/hooks/index.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ Choosing a hook
3434
* - Hook
3535
- When to use
3636
* - :class:`~airflow.providers.common.ai.hooks.pydantic_ai.PydanticAIHook`
37-
- Default for ``common.ai`` operators (``LLMOperator``, ``AgentOperator``,
38-
``LLMBranchOperator``, ...). Returns a pydantic-ai ``Agent`` / ``Model``.
37+
- Default for one-shot LLM operators (``LLMOperator``, ``LLMBranchOperator``, ...).
38+
Also used by ``AgentOperator`` when ``conn_type`` is ``pydanticai`` (or
39+
``pydanticai-bedrock``, ``pydanticai-azure``, ``pydanticai-vertex``).
3940
* - :class:`~airflow.providers.common.ai.hooks.langchain.LangChainHook`
4041
- Direct LangChain access for tasks that compose ``Runnable``\\s, use the
4142
LangChain agent surface, or need LangChain-native chat / embedding model

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ a single prompt and returns the output. ``AgentOperator`` manages a stateful
3131
tool-call loop where the LLM decides which tools to call and when to stop.
3232

3333
.. seealso::
34-
:ref:`Connection configuration <howto/connection:pydanticai>`
34+
:ref:`Pydantic AI connection <howto/connection:pydanticai>`
35+
36+
The agent backend is selected by the Airflow connection ``conn_type`` (for example
37+
``pydanticai``, ``pydanticai-bedrock``, or ``pydanticai-azure``). You do not choose a different operator class.
3538

3639

3740
SQL Agent
@@ -251,11 +254,17 @@ Parameters
251254
templating.
252255
- ``output_type``: Expected output type (default: ``str``). Set to a Pydantic
253256
``BaseModel`` for structured output.
254-
- ``toolsets``: List of pydantic-ai toolsets (``SQLToolset``, ``HookToolset``,
255-
etc.).
256-
- ``enable_tool_logging``: Wrap each toolset in
257-
:class:`~airflow.providers.common.ai.toolsets.logging.LoggingToolset` so that
258-
every tool call is logged in real time. Default ``True``.
257+
- ``toolsets``: List of toolsets the agent can use. Accepts
258+
:class:`~airflow.providers.common.ai.hooks.base_ai.BaseToolset` subclasses
259+
(``SQLToolset``), pydantic-ai ``AbstractToolset`` implementations
260+
(``HookToolset``, ``MCPToolset``, ``DataFusionToolset``, third-party toolsets),
261+
plain Python callables, or native pydantic-ai ``Tool`` objects. Mixed lists
262+
are supported.
263+
- ``enable_tool_logging``: When ``True`` (default), wraps each tool call with
264+
real-time logging. For pydantic-ai ``AbstractToolset`` items this is done via
265+
:class:`~airflow.providers.common.ai.toolsets.logging.LoggingToolset`; for
266+
plain callables and :class:`~airflow.providers.common.ai.hooks.base_ai.BaseToolset`
267+
items it is applied at the callable level.
259268
- ``agent_params``: Additional keyword arguments passed to the pydantic-ai
260269
``Agent`` constructor (e.g. ``retries``, ``model_settings``).
261270
- ``usage_limits``: Optional pydantic-ai ``UsageLimits`` enforced on every
@@ -272,9 +281,9 @@ Parameters
272281
Logging
273282
-------
274283

275-
All AI operators automatically log a post-run summary after ``run_sync()``
276-
completes. ``AgentOperator`` additionally wraps toolsets for real-time
277-
per-tool-call logging (controlled by ``enable_tool_logging``).
284+
All AI operators automatically log a post-run summary after the agent run
285+
completes. ``AgentOperator`` additionally provides real-time per-tool-call
286+
logging (controlled by ``enable_tool_logging``).
278287

279288
**Real-time tool call logging** (AgentOperator only) — each tool call is
280289
logged as it happens:

providers/common/ai/docs/toolsets.rst

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,41 @@ Three toolsets are included:
3434
`MCP servers <https://modelcontextprotocol.io/>`__ configured via Airflow
3535
connections.
3636

37-
All three implement pydantic-ai's
38-
`AbstractToolset <https://ai.pydantic.dev/toolsets/>`__ interface and can be
39-
passed to any pydantic-ai ``Agent``, including via
40-
:class:`~airflow.providers.common.ai.operators.agent.AgentOperator`.
37+
:class:`~airflow.providers.common.ai.toolsets.hook.HookToolset` and
38+
:class:`~airflow.providers.common.ai.toolsets.mcp.MCPToolset` implement pydantic-ai's
39+
`AbstractToolset <https://ai.pydantic.dev/toolsets/>`__ interface.
40+
:class:`~airflow.providers.common.ai.toolsets.sql.SQLToolset` implements the
41+
framework-agnostic :class:`~airflow.providers.common.ai.hooks.base_ai.BaseToolset` interface.
42+
All three can be passed to
43+
:class:`~airflow.providers.common.ai.operators.agent.AgentOperator`, which routes each
44+
toolset to the correct agent parameter automatically.
4145

4246
.. note::
4347

44-
``AgentOperator`` accepts **any** ``AbstractToolset`` implementation — not
45-
just the Airflow-native toolsets above. PydanticAI's own MCP server
46-
classes (``MCPServerStreamableHTTP``, ``MCPServerSSE``, ``MCPServerStdio``)
47-
and third-party toolsets work too. The Airflow-native toolsets add
48-
connection management, secret backend integration, and the connection UI,
49-
but you are not locked in.
48+
``AgentOperator`` accepts a mixed ``toolsets`` list containing any
49+
combination of:
5050

51+
- pydantic-ai ``AbstractToolset`` implementations (``HookToolset``,
52+
``MCPToolset``, ``DataFusionToolset``).
53+
- Any third-party ``AbstractToolset``, including PydanticAI's own MCP
54+
server classes (``MCPServerStreamableHTTP``, ``MCPServerSSE``,
55+
``MCPServerStdio``).
56+
- :class:`~airflow.providers.common.ai.hooks.base_ai.BaseToolset`
57+
subclasses (``SQLToolset``).
58+
- Plain Python callables (``def my_tool(...): ...``).
59+
- Native pydantic-ai ``Tool`` objects.
5160

52-
Using Toolsets Directly with PydanticAI
53-
---------------------------------------
61+
The hook routes each item to the correct agent parameter automatically.
5462

55-
Toolsets are standard pydantic-ai ``AbstractToolset`` implementations with no
56-
dependency on ``AgentOperator`` or ``@task.agent``. You can use them anywhere
57-
you can run Python within Airflow -- ``@task`` functions, ``PythonOperator``
58-
callables, or any custom operator's ``execute()`` method -- by creating a
59-
``pydantic_ai.Agent`` yourself:
63+
64+
Using Toolsets Directly
65+
-----------------------
66+
67+
Toolsets can be used anywhere you can run Python within Airflow — ``@task``
68+
functions, ``PythonOperator`` callables, or any custom operator's
69+
``execute()`` method — without needing ``AgentOperator`` or ``@task.agent``.
70+
Pass toolsets via :class:`~airflow.providers.common.ai.hooks.base_ai.AgentRunRequest`
71+
and call the hook yourself:
6072

6173
.. exampleinclude:: /../../ai/src/airflow/providers/common/ai/example_dags/example_pydantic_ai_hook.py
6274
:language: python
@@ -67,11 +79,13 @@ This works because toolsets resolve Airflow connections lazily via
6779
``BaseHook.get_connection()``, which is available in any task execution
6880
context.
6981

70-
This approach gives you full control over the agent lifecycle -- you can call
71-
``agent.run_sync()`` multiple times, swap models at runtime, or combine
72-
results from several agents in a single task. The tradeoff is that you lose
82+
This approach gives you direct control over the agent lifecycle you can
83+
build and run multiple agents in a single task, or combine results from
84+
several runs. The tradeoff is that you lose
7385
the durable execution (step-level caching with retry replay), HITL review
74-
integration, and automatic tool call logging that ``AgentOperator`` provides.
86+
integration, and the automatic tool call logging and routing that
87+
``AgentOperator`` provides via
88+
:class:`~airflow.providers.common.ai.toolsets.logging.LoggingToolset`.
7589

7690

7791
``HookToolset``

providers/common/ai/src/airflow/providers/common/ai/example_dags/example_pydantic_ai_hook.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17-
"""Example DAGs demonstrating PydanticAIHook and direct pydantic-ai Agent usage."""
17+
"""Example DAGs demonstrating BaseAIHook and AgentRunRequest usage."""
1818

1919
from __future__ import annotations
2020

2121
from pydantic import BaseModel
2222

23-
from airflow.providers.common.ai.hooks.pydantic_ai import PydanticAIHook
23+
from airflow.providers.common.ai.hooks.base_ai import AgentRunRequest, BaseAIHook
2424
from airflow.providers.common.compat.sdk import dag, task
2525

2626

@@ -29,9 +29,10 @@
2929
def example_pydantic_ai_hook():
3030
@task
3131
def generate_summary(text: str) -> str:
32-
hook = PydanticAIHook(llm_conn_id="pydanticai_default")
33-
agent = hook.create_agent(output_type=str, instructions="Summarize concisely.")
34-
result = agent.run_sync(text)
32+
hook = BaseAIHook.get_agent_hook("pydanticai_default")
33+
request = AgentRunRequest(prompt=text, output_type=str, instructions="Summarize concisely.")
34+
agent = hook.create_agent(request)
35+
result = hook.run_agent(agent, request)
3536
return result.output
3637

3738
generate_summary("Apache Airflow is a platform for programmatically authoring...")
@@ -51,12 +52,14 @@ class SQLResult(BaseModel):
5152
query: str
5253
explanation: str
5354

54-
hook = PydanticAIHook(llm_conn_id="pydanticai_default")
55-
agent = hook.create_agent(
55+
hook = BaseAIHook.get_agent_hook("pydanticai_default")
56+
request = AgentRunRequest(
57+
prompt=prompt,
5658
output_type=SQLResult,
5759
instructions="Generate a SQL query and explain it.",
5860
)
59-
result = agent.run_sync(prompt)
61+
agent = hook.create_agent(request)
62+
result = hook.run_agent(agent, request)
6063
return result.output.model_dump()
6164

6265
generate_sql("Find the top 10 customers by revenue")
@@ -76,8 +79,9 @@ def example_task_with_toolsets():
7679
def analyze_revenue() -> str:
7780
from airflow.providers.common.ai.toolsets.sql import SQLToolset
7881

79-
hook = PydanticAIHook(llm_conn_id="pydanticai_default")
80-
agent = hook.create_agent(
82+
hook = BaseAIHook.get_agent_hook("pydanticai_default")
83+
request = AgentRunRequest(
84+
prompt="Which customers have spent the most? Show the top 5.",
8185
output_type=str,
8286
instructions=(
8387
"You are a sales analytics assistant. "
@@ -91,7 +95,8 @@ def analyze_revenue() -> str:
9195
),
9296
],
9397
)
94-
result = agent.run_sync("Which customers have spent the most? Show the top 5.")
98+
agent = hook.create_agent(request)
99+
result = hook.run_agent(agent, request)
95100
return result.output
96101

97102
analyze_revenue()

0 commit comments

Comments
 (0)