kaxil opened a new pull request, #76752:
URL: https://github.com/apache/airflow/pull/76752

   `AirflowTools(SQLToolset(...).filtered(...))` failed with `AttributeError: 
'FilteredToolset' object has no attribute 'airflow_tools'`, in every adapter 
(OpenAI Agents SDK, Strands, ADK, the Anthropic tool runner, the Claude Agent 
SDK). `.filtered()` is how the toolsets and MCP guides tell users to offer only 
some tools, so moving from `AgentOperator` to an agent of your own lost it, and 
the OpenAI Agents guide told users to fall back on `allowed_tables` and 
`allowed_methods` instead.
   
   `collect_tools()`, which every adapter goes through, now reads through 
`.filtered()`, `.prefixed()` and `.renamed()`, nested in any order. Any other 
wrapper raises a `TypeError` that names it and says what to do instead; for 
`.approval_required()` that is `AgentOperator`, since nothing pauses a 
non-Pydantic AI run for Airflow's approval.
   
   ```python
   tools = AirflowTools(SQLToolset(db_conn_id="warehouse").filtered(lambda ctx, 
tool: tool.name != "query"))
   ```
   
   ## Design rationale
   
   **The tools still come from the inner toolset's own `airflow_tools()`.** The 
walk strips the three wrappers down to the innermost toolset, takes its 
`AirflowTool`s, and asks the outermost wrapper's own `get_tools()` which of 
them are offered and under what name. So pydantic-ai runs the filter (sync or 
async) and builds the names as it would in an agent run, with the toolset's 
real `ToolDefinition`s. Calling `airflow_tools_from_toolset(wrapper)` and 
letting each wrapper's `call_tool` route the call would be shorter, but it 
skips the inner toolset's `airflow_tools()`, which `MCPToolset` uses to refuse 
and a third-party toolset may override. What this costs is about ten lines that 
map an offered name back through each prefix and rename, mirroring the 
wrappers' `call_tool`; `test_nested_wrappers_apply_from_the_inside_out` fails 
if that mapping goes wrong.
   
   **The filter runs once, when the adapter is built.** It gets the same inert 
`RunContext` `airflow_tools_from_toolset` already passes (`deps=None`), so a 
filter that reads `ctx.deps` fails at build time, and whatever it decides holds 
for every run, as the OpenAI Agents guide now says.
   
   **Sources that work today keep working.** No common.ai toolset is a 
`WrapperToolset` that also has `airflow_tools()`; `MaskingToolset`, the durable 
journal wrapper and the logging wrapper already failed with `AttributeError`. 
Any source with an `airflow_tools()` method, a wrapper included, is still asked 
for its tools first, and the method is looked up rather than checked against 
the `ToolProvider` Protocol, since on Python 3.12+ a Protocol `isinstance` 
misses a method a mock or proxy provides through `__getattr__` (both cases are 
tested). Only exact `FilteredToolset`, `PrefixedToolset` and `RenamedToolset` 
are read through: a subclass could add behaviour in `call_tool` that tools 
reached around it would skip, so it gets the `TypeError`.
   
   **The adapters take a new `ToolSource` alias** (`ToolProvider | AirflowTool 
| FilteredToolset | PrefixedToolset | RenamedToolset`), so mypy accepts the 
three wrappers and still rejects `.approval_required()` passed directly.
   
   The duplicate-tool-name error now also suggests `.prefixed()`, which is the 
fix for two `SQLToolset`s on one agent, since `SQLToolset` has no `tool_prefix`.
   
   Checked on `openai-agents` 0.20.0 with `openai` 2.54 (what the workspace 
holds) and 0.24.0 with `openai` 3.42 (`run_agent_framework_tests.sh 
openai-agents --framework-pins`), and on pydantic-ai-slim 2.36.0, the 
provider's floor, whose wrapper attributes (`wrapped`, `filter_func`, `prefix`, 
`name_map`) match the current release.
   
   End to end, a plain `@task` built an OpenAI Agents SDK agent (0.20.0, 
`gpt-5.4-mini` through an OpenAI-compatible gateway) with 
`AirflowTools(SQLToolset(...).filtered(lambda ctx, tool: tool.name != 
"query"))` over a SQLite database, under `airflow dags test`. An `httpx` hook 
logged the tools in each request the SDK sent to the model:
   
   ```text
   AirflowTools holds: ['list_tables', 'get_schema', 'check_query']
   Request to /llm/v1/chat/completions offered the model these tools: 
['list_tables', 'get_schema', 'check_query']
   ::group::Tool call: list_tables
   ::group::Tool call: get_schema
   Request to /llm/v1/chat/completions offered the model these tools: 
['list_tables', 'get_schema', 'check_query']
   Final answer: The database has these tables:
   - `customers`
   - `orders`
   [...]
   I do not have a tool that runs a SQL query here, so I can’t count how many 
orders there are.
   Dag run in success state
   ```
   
   The trimmed lines list the four `orders` columns with their types, all 
correct.
   
   ## Known issues
   
   Under `.prefixed()` or `.renamed()`, a tool that keeps failing raises 
`ToolCallError("query kept failing ...")` with its inner name, while the task 
log's `Tool call:` line shows the offered name (`wh_query`). The message comes 
from the inner toolset's tool, which this change reuses rather than rebuilds.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to