Lee-W commented on code in PR #76752:
URL: https://github.com/apache/airflow/pull/76752#discussion_r4532811115
##########
providers/common/ai/src/airflow/providers/common/ai/tools/adk.py:
##########
@@ -55,7 +55,8 @@ class AirflowTools(BaseToolset):
Pass toolsets that implement
:class:`~airflow.providers.common.ai.tools.ToolProvider`, such as
``SQLToolset``
- and ``HookToolset``, or individual
+ and ``HookToolset``, optionally wrapped with ``.filtered()``,
Review Comment:
Nit: a `.filtered()` callback that reads `ctx.deps` fails with the bare
`AttributeError` from the user’s own lambda, because the filter runs once at
construction with `deps=None`. Only the OpenAI Agents guide and the
`collect_tools` docstring say so, and none of the five adapter docstrings do.
We can add one sentence next to "optionally wrapped with .filtered()…" in
each adapter (e.g, "A .filtered() function runs once, when the adapter is
built, with ctx.deps set to None."
##########
providers/common/ai/src/airflow/providers/common/ai/tools/_from_toolset.py:
##########
@@ -88,12 +93,104 @@ def airflow_tools_from_toolset(toolset:
AbstractToolset[Any], *, deps: Any = Non
:param toolset: The pydantic-ai toolset to expose.
:param deps: Exposed to the toolset as ``ctx.deps``.
"""
- ctx: RunContext[Any] = RunContext(deps=deps, model=TestModel(),
usage=RunUsage(), max_retries=1)
+ ctx = _build_inert_context(deps)
toolset_tools = run_coroutine_sync(toolset.get_tools(ctx))
in_order = _InOrder()
return [_as_airflow_tool(toolset, name, tool, ctx, in_order) for name,
tool in toolset_tools.items()]
+def _build_inert_context(deps: Any = None) -> RunContext[Any]:
+ return RunContext(deps=deps, model=TestModel(), usage=RunUsage(),
max_retries=1)
+
+
+# The wrappers that only choose which tools the model is offered, and under
what name.
+_NAMING_WRAPPERS = (FilteredToolset, PrefixedToolset, RenamedToolset)
+
+
+def _is_naming_wrapper(
+ toolset: object,
+) -> TypeGuard[FilteredToolset[Any] | PrefixedToolset[Any] |
RenamedToolset[Any]]:
+ # Exact types: the tools reached around a wrapper skip its call_tool,
which a subclass may override.
+ return type(toolset) in _NAMING_WRAPPERS
+
+
+def resolve_airflow_tools(source: object) -> list[AirflowTool]:
+ """
+ Return the tools of a toolset a framework adapter was given.
+
+ A source with an ``airflow_tools()`` method is asked for them. A toolset
wrapped with
+ ``.filtered()``, ``.prefixed()`` or ``.renamed()``, nested in any order,
gives the innermost
+ toolset's own ``airflow_tools()``, so they behave as they do without the
wrappers. The
+ wrappers' own ``get_tools`` decides which of them the model is offered and
under what name,
+ given the same inert ``RunContext`` :func:`airflow_tools_from_toolset`
passes. A ``.filtered()``
+ function therefore runs once, when the adapter is built, rather than
before every model turn
+ as in a pydantic-ai run.
+
+ :raises TypeError: when ``source`` is any other wrapper, such as
``.approval_required()``,
+ or there is no ``airflow_tools()`` method under its wrappers.
+ :raises ValueError: when a ``.renamed()`` map sends an offered name to a
name the toolset does
+ not have, such as a map written backwards.
+ """
+ # Looked up, not checked against ToolProvider: since Python 3.12 a
Protocol check misses a
+ # method that a mock or proxy only provides through __getattr__.
+ own = getattr(source, "airflow_tools", None)
+ if callable(own):
+ return list(own())
+ if not _is_naming_wrapper(source):
+ raise TypeError(_describe_unsupported_toolset(source))
+ renames: list[PrefixedToolset[Any] | RenamedToolset[Any]] = []
+ inner: AbstractToolset[Any] = source
+ while _is_naming_wrapper(inner):
+ if not isinstance(inner, FilteredToolset):
+ renames.append(inner)
+ inner = inner.wrapped
+ provide = getattr(inner, "airflow_tools", None)
+ if not callable(provide):
+ raise TypeError(_describe_unsupported_toolset(inner))
+ by_name = {tool.name: tool for tool in provide()}
+ offered = run_coroutine_sync(source.get_tools(_build_inert_context()))
+ tools = []
+ for name in offered:
+ # Map the offered name back through each wrapper, as its ``call_tool``
does.
Review Comment:
Nit: this comment only narrates the loop below it. We can drop it, or keep
the part that is not obvious from the code: that `PrefixedToolset` and
`RenamedToolset` are undone in the same order their `call_tool` does.
--
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]