Lee-W commented on code in PR #72936:
URL: https://github.com/apache/airflow/pull/72936#discussion_r4045938192


##########
providers/common/ai/docs/choosing_a_toolset.rst:
##########
@@ -0,0 +1,533 @@
+ .. Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+ ..   http://www.apache.org/licenses/LICENSE-2.0
+
+ .. Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+.. _howto/choosing-a-toolset:
+
+Choosing a Toolset
+==================
+
+:doc:`toolsets` documents how to configure each toolset. This page answers the
+question that comes before that one: you have a system you want an agent to
+reach, so which route do you take, and what does each route give up?
+
+Read the table below by what you already have, not by what a toolset is called.
+When two routes both work, the deciding factor is rarely what each one can do —
+it is what each one cannot do, and every route has a short list.
+
+More than one row can be true at once, and the rows are not exclusive: one 
agent
+can carry several toolsets. Two questions break the ties. *Whose credential is
+it?* — prefer the route whose credential is an Airflow connection somebody on
+your side already reviewed. *Whose tool list is it?* — prefer the route whose
+exposed surface you chose rather than inherited. The pair that most often
+overlaps is an Airflow hook and a vendor MCP server reaching the same target;
+both questions point at the hook, because its credential is the connection and
+``allowed_methods`` is a list you write. Reach for the server when its tools
+cover work the hook does not expose, or when the alternative is re-wrapping 
that
+API by hand.
+
+Those two questions do not separate ``HookToolset`` from ``SQLToolset`` when 
the
+target is a DBAPI database, because both answer them the same way. A third one
+does: *is the work a fixed operation or an open-ended question?* A named method
+you can enumerate in advance is a hook. A question the agent has to express as
+SQL is a query, and ``SQLToolset`` answers it with schema discovery, bounded
+results and an ``allowed_tables`` walk you can switch on, none of which
+``HookToolset`` has an equivalent of.
+
+Start with what you have
+------------------------
+
+.. list-table::
+   :widths: 50 50
+   :header-rows: 1
+
+   * - What you have
+     - Route
+   * - A target that already has an Airflow connection, and a hook method that
+       already does the thing
+     - ``HookToolset``
+   * - A question that is a query, against a DBAPI database
+     - ``SQLToolset``
+   * - Files on an object store — Parquet, CSV, Avro, Iceberg — rather than 
rows
+       in a database
+     - ``DataFusionToolset``
+   * - A vendor that already ships a server built for agents, whose tools you
+       would otherwise re-wrap by hand
+     - ``MCPToolset``
+   * - Procedural knowledge — how to carry out a task — rather than an endpoint
+       to call
+     - ``AgentSkillsToolset``
+   * - Work that means running code the model wrote, not calling a tool you 
chose
+     - ``SandboxToolset``
+   * - Reasoning that should happen on the vendor's own infrastructure
+     - A subclass of ``BaseManagedAgentToolset`` that you write
+
+The rest of this page takes those seven in turn. Each entry gives the case for
+choosing it, what it cannot do, an example that exists in this repository, and
+where its credentials and its work come from.
+
+``HookToolset``
+---------------
+
+**Choose it when** the target already has an Airflow connection and a hook, and
+what you want the agent to do is already a method on that hook. This is the
+cheapest route — no new server, no new credential, no new query dialect — and
+the only one that reaches any provider hook with synchronous methods without
+anyone writing an adapter first. 
:class:`~airflow.providers.common.ai.toolsets.hook.HookToolset` is a
+reflection-based adapter, so the work is choosing the method list.
+
+**What it cannot do**
+
+- It allow-lists method *names*, not arguments. Once ``read_key`` is exposed,
+  the agent picks the key; the :ref:`defense-layer table 
<toolset-defense-layers>`
+  states this outright. Choose methods whose worst case you accept, not methods
+  you intend to constrain later.
+- Its calls act as barriers. The tools are registered with ``sequential=True``
+  because hook methods perform synchronous I/O, so a slow call holds up every
+  other tool the model emitted in that step, not only this toolset's. This is
+  not specific to ``HookToolset`` — see :ref:`toolset-call-barriers`.
+- It returns exactly one shape. Every result goes through ``serialize_for_llm``
+  and comes back as a JSON-encoded string; there is no structured error type 
and
+  no ``ModelRetry`` wrapper, so a hook exception fails the agent run, and the
+  task with it, instead of giving the model something it can correct.
+  ``SQLToolset``, by contrast, hands the database's own error back as a retry.
+- Its ``call_tool`` calls the method and serializes what comes back. The code
+  contains no path that awaits a coroutine result, and none that checks for 
one,
+  so an ``async def`` hook method is not a case this adapter is written to
+  handle. Treat synchronous methods as the supported set.
+
+**A real example.** The read-only S3 pair from the ``HookToolset`` guidance in
+:doc:`toolsets`:
+
+.. code-block:: python
+
+    HookToolset(
+        s3_hook,
+        allowed_methods=["list_keys", "read_key"],
+        tool_name_prefix="s3_",
+    )
+
+**Credentials and where it runs.** The hook instance is yours, so the 
credential
+is whatever connection that hook resolves — the toolset never looks one up
+itself. Calls run in the Airflow worker process.
+
+``SQLToolset``
+--------------
+
+**Choose it when** the question is a query and the data is in a DBAPI database.
+:class:`~airflow.providers.common.ai.toolsets.sql.SQLToolset` gives the agent
+four tools — list tables, get schema, query, check query. Set 
``allowed_tables``
+and that allow-list is enforced by parsing the SQL rather than by matching
+strings; see :ref:`allowed-tables-enforcement` for how the walk handles CTEs,
+subqueries and joins.
+
+**What it cannot do**
+
+- ``allowed_tables`` is an application-level guardrail, not a replacement for
+  database permissions. Its own docstring says so, and names the residual gap:
+  an engine or query the parser reads differently. Point ``db_conn_id`` at a
+  least-privilege role whose grants match the allow-list.
+- It cannot bound the fetch for every driver. Hooks that hand their handler
+  something other than a DBAPI cursor — ``ExasolHook`` and its pyexasol
+  statement, for instance — fall back to a full fetch. The payload handed to 
the
+  model is still bounded; the transfer is not. See 
:ref:`bounded-query-results`.
+- Its parser-level closure is opt-in, not the default. ``allowed_tables``
+  defaults to ``None`` and the table walk returns immediately while it is 
unset,
+  so out of the box the agent reaches every table the connection can see.
+  ``DESCRIBE`` and ``SHOW`` pass as well, on dialects that parse them, because
+  read-only metadata statements are allowed deliberately. Set 
``allowed_tables``
+  and the walk turns fail-closed: dynamic SQL, the ``TABLE <name>`` shorthand 
and

Review Comment:
   `SHOW` is now named as fail-closed once `allowed_tables` is set, with 
`DESCRIBE`'s different behaviour spelled out — it becomes an ordinary table 
reference, allowed only when the table it names is on the list.
   The construct enumeration is removed; the page defers to How 
`allowed_tables` Is Enforced for the rest, which keeps a second list from 
drifting against the code. The failure-classification bullet now names the two 
paths that do not raise: `check_query`, which reports `{"valid": false, ...}` 
itself, and `get_schema`, which returns `{"error": ...}` on an allow-list miss.
   
   I kept only the `SHOW` / `DESCRIBE` split, since the page raises both two 
sentences earlier and dropping it would leave that hanging.
   



-- 
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