kaxil opened a new pull request, #73587: URL: https://github.com/apache/airflow/pull/73587
Common AI's toolsets (`SQLToolset`, `HookToolset`, ...) are pydantic-ai toolsets, so today only `AgentOperator` and pydantic-ai agents can use them. Teams that already build agents with [Strands Agents](https://strandsagents.com/) have to rewrite the agent for `AgentOperator` or hand-write tools around hooks and lose the toolsets' SQL validation, table allowlists and bounded results. This adds `as_strands_tools()`, which turns those toolsets into Strands tools. The user keeps a plain Strands `Agent`, with their own model, loop and Strands features, and Airflow supplies connection-backed tools: ```python agent = Agent(model=model, tools=as_strands_tools(SQLToolset(db_conn_id="warehouse"))) ``` It sits on a small framework-neutral interface in `airflow.providers.common.ai.tools`: `AirflowTool` (name, description, JSON Schema, async call), `ToolResult` (JSON-compatible content plus an explicit `is_error`), and `ToolProvider.airflow_tools()`, which `SQLToolset` and `HookToolset` now implement. Both the interface and the adapter are marked experimental. Strands is an optional dependency imported only by the adapter module, and nothing changes for `AgentOperator` users. ## Design rationale **Why not a `StrandsAgentOperator`?** An operator would own the agent loop, and every Strands feature would then need an Airflow release to reach users. The adapter only converts tools, so the agent stays native and Strands releases reach users without an Airflow change. **Masking lives in the one path every adapter shares.** Airflow's secret masker protects task logs, not tool results. A Strands tool whose client library puts a credentialed URL in an error message sends the raw password to the model provider, into the agent's answer and into its traces, while the task log shows `***`. I reproduced this with a native Strands `@tool` in a scheduler-run task. `AirflowTool.call` turns exceptions into error results and runs `redact()` on every result and error before anything reaches the framework, matching values in nested JSON up to 32 levels deep rather than the masker's default of 5. An adapter for another framework only maps the three types. **Why an explicit `airflow_tools()` per toolset instead of accepting any `AbstractToolset`?** The bridge calls the toolset's `call_tool` with an inert `RunContext`, as the existing LangChain bridge does. That is only correct for toolsets that ignore the context, which `SQLToolset` and `HookToolset` do. Opting toolsets in one at a time keeps the contract true. `MCPToolset` would reconnect on every call, and a custom toolset that reads `ctx.model` or `ctx.messages` would misbehave. **Calls into one toolset instance are serialised.** Both toolsets share one hook doing blocking I/O, and `SQLToolset` reads `hook.last_description` after each query, which is why their pydantic-ai tool definitions are marked `sequential`. Agent frameworks can run tool calls concurrently, and several agents can share one toolset, so the bridge keeps one lock per toolset instance and runs the hook call off the framework's event loop. **The adapter copies the input schema.** Strands fills in missing property types and descriptions in place when it registers a tool, and the source schema is a module-level constant the pydantic-ai path also uses. The LangChain bridge's private coroutine helper moves to `utils/coroutines.py` so both bridges share it. It is unchanged. The example Dag in this PR ran unmodified as a scheduler-run task on Airflow main with Postgres against `claude-sonnet-5`. The agent called `list_tables`, then `query`, and returned the correct row counts in 7.5 s. The screenshot filters the log to the tool, hook and operator sources:  ## Known issues - **The Strands adapter tests skip in CI.** `strands-agents` pins `mcp<2.2`, and adding it to the dev group downgrades `mcp` from 2.2.0 to 2.1.1 in the workspace lock, so it is not added. `test_strands.py` uses `pytest.importorskip`. Locally, with `strands-agents==1.56.0` installed, all touched suites pass (172 tests). - **Resolving `strands-agents` with no lower bound can pick the `0.0.1` placeholder release.** The docs tell users to install `strands-agents>=1.56.0`. - **Strands' own `anthropic` extra requires `anthropic<1`**, which conflicts with this provider's `anthropic` extra. `AnthropicModel` works with `anthropic` 1.x at runtime, so the docs point users at this provider's extra. ## Follow-ups - Build the LangChain bridge on `AirflowTool` so its tools are masked too. - Mask the retry message `SQLToolset` returns to pydantic-ai agents, which today carries the database's error text unmasked. --- * Read the **[Pull Request Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)** for more information. Note: commit author/co-author name and email in commits become permanently public when merged. * For fundamental code changes, an Airflow Improvement Proposal ([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals)) is needed. * When adding dependency, check compliance with the [ASF 3rd Party License Policy](https://www.apache.org/legal/resolved.html#category-x). * For significant user-facing changes create newsfragment: `{pr_number}.significant.rst`, in [airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments). You can add this file in a follow-up commit after the PR is created so you know the PR number. -- 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]
