Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion providers/common/ai/docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ Extra Dependencies
``openai`` ``pydantic-ai-slim[openai]``
``mcp`` ``pydantic-ai-slim[mcp]``
``code-mode`` ``pydantic-ai-harness[codemode]>=0.3.0``
``skills`` ``apache-airflow-providers-git>=0.4.0``, ``pydantic-ai-skills>=0.11.0``
``skills`` ``apache-airflow-providers-git>=0.4.0``, ``pydantic-ai-skills>=1.2.0``
``avro`` ``fastavro>=1.10.0; python_version < "3.14"``, ``fastavro>=1.12.1; python_version >= "3.14"``
``parquet`` ``pyarrow>=18.0.0; python_version < '3.14'``, ``pyarrow>=22.0.0; python_version >= '3.14'``
``sql`` ``apache-airflow-providers-common-sql``, ``sqlglot>=30.0.0``
Expand Down
10 changes: 10 additions & 0 deletions providers/common/ai/docs/toolsets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,16 @@ Parameters
:class:`~airflow.providers.common.ai.skills.GitSkills`.
- ``exclude_tools``: Optional set of skill tool names to hide from the agent
(e.g. ``{"run_skill_script"}`` to disable on-worker script execution).
- ``exclude_resources``: Optional glob patterns to exclude from resource
discovery, added on top of the built-in defaults (``__pycache__``, ``*.pyc``,
``*.pyo``, ``.DS_Store``, ``.git``). A skill exposes every readable text file
it contains as a resource; these patterns keep matched files out of the
resource list and the ``read_skill_resource`` tool (e.g.
``["*.env", "secrets/*"]``). Each pattern matches the full skill-relative path
or any single path component. This hides files from resource discovery only --
it does not stop a skill's ``run_skill_script`` from reading them off disk, so
pair it with ``exclude_tools={"run_skill_script"}`` when the files are
genuinely sensitive.

Using Agent Skills with other frameworks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
4 changes: 2 additions & 2 deletions providers/common/ai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ dependencies = [
# upstream in pydantic/pydantic-ai#5230; revisit this extra once that lands.
"skills" = [
"apache-airflow-providers-git>=0.4.0",
"pydantic-ai-skills>=0.11.0",
"pydantic-ai-skills>=1.2.0",
]
"avro" = [
'fastavro>=1.10.0; python_version < "3.14"',
Expand Down Expand Up @@ -136,7 +136,7 @@ dev = [
# Additional devel dependencies (do not remove this line and add extra development dependencies)
"sqlglot>=30.0.0",
"pydantic-ai-slim[mcp]",
"pydantic-ai-skills>=0.11.0",
"pydantic-ai-skills>=1.2.0",
"apache-airflow-providers-common-sql[datafusion]",
"langchain>=1.0.0",
"llama-index-core>=0.13.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ class AgentSkillsToolset(AbstractToolset):
:param sources: Skill sources -- local directory paths and/or ``GitSkills``.
:param exclude_tools: Optional set of skill tool names to hide from the agent
(e.g. ``{"run_skill_script"}`` to disable on-worker script execution).
:param exclude_resources: Optional glob patterns to exclude from resource
discovery, added on top of the built-in defaults (``__pycache__``,
``*.pyc``, ``*.pyo``, ``.DS_Store``, ``.git``). A skill exposes every
readable text file it contains as a resource; these patterns keep matched
files out of the resource list and the ``read_skill_resource`` tool
(e.g. ``["*.env", "secrets/*"]``). Patterns match the full skill-relative
path or any single path component. This hides files from resource
discovery only -- it does not stop a skill's ``run_skill_script`` from
reading them off disk, so pair it with ``exclude_tools={"run_skill_script"}``
when the files are genuinely sensitive. Requires ``pydantic-ai-skills>=1.2.0``.

Requires the ``skills`` extra: ``pip install "apache-airflow-providers-common-ai[skills]"``.
"""
Expand All @@ -66,9 +76,11 @@ def __init__(
sources: list[SkillSource],
*,
exclude_tools: set[str] | None = None,
exclude_resources: list[str] | None = None,
) -> None:
self._sources = list(sources)
self._exclude_tools = exclude_tools
self._exclude_resources = exclude_resources
self._inner: Any = None
self._cleanup: Callable[[], None] | None = None

Expand All @@ -80,7 +92,11 @@ async def for_run(self, ctx: RunContext) -> AbstractToolset:
# Per-run isolation: pydantic-ai shares one toolset instance across runs,
# but we hold per-run clone/cleanup state on __aenter__/__aexit__. Hand
# each run its own instance so concurrent runs never clobber each other.
return AgentSkillsToolset(self._sources, exclude_tools=self._exclude_tools)
return AgentSkillsToolset(
self._sources,
exclude_tools=self._exclude_tools,
exclude_resources=self._exclude_resources,
)

async def __aenter__(self) -> AgentSkillsToolset:
# Resolve + clone at run time, on the worker -- not at DAG-parse time.
Expand All @@ -98,6 +114,8 @@ async def __aenter__(self) -> AgentSkillsToolset:
kwargs: dict[str, Any] = {"directories": directories}
if self._exclude_tools:
kwargs["exclude_tools"] = self._exclude_tools
if self._exclude_resources:
kwargs["exclude_resources"] = self._exclude_resources
self._inner = SkillsToolset(**kwargs)
await self._inner.__aenter__()
except BaseException:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,56 @@ def fake_skillstoolset(**kwargs):
assert captured["exclude_tools"] == {"run_skill_script"}
assert captured["directories"] == ["/x"]

def test_exclude_resources_passed_to_inner(self):
captured: dict = {}

def fake_skillstoolset(**kwargs):
captured.update(kwargs)
return _FakeInner(**kwargs)

toolset = AgentSkillsToolset(sources=["/x"], exclude_resources=["*.env", "secrets/*"])
with patch(
"airflow.providers.common.ai.toolsets.skills._materialize_skills",
autospec=True,
return_value=(["/x"], lambda: None),
):
with patch("pydantic_ai_skills.SkillsToolset", fake_skillstoolset): # noqa: spec
asyncio.run(_enter_exit(toolset))

assert captured["exclude_resources"] == ["*.env", "secrets/*"]

def test_no_optional_kwargs_when_unset(self):
# Omit exclude_tools/exclude_resources entirely so an older pydantic-ai-skills
# that lacks the kwarg still works when the feature is not used.
captured: dict = {}

def fake_skillstoolset(**kwargs):
captured.update(kwargs)
return _FakeInner(**kwargs)

toolset = AgentSkillsToolset(sources=["/x"])
with patch(
"airflow.providers.common.ai.toolsets.skills._materialize_skills",
autospec=True,
return_value=(["/x"], lambda: None),
):
with patch("pydantic_ai_skills.SkillsToolset", fake_skillstoolset): # noqa: spec
asyncio.run(_enter_exit(toolset))

assert "exclude_tools" not in captured
assert "exclude_resources" not in captured

def test_for_run_propagates_optional_kwargs(self):
# for_run hands each run its own instance; dropping a kwarg here would
# silently expose excluded files in concurrent runs.
toolset = AgentSkillsToolset(
sources=["/x"], exclude_tools={"run_skill_script"}, exclude_resources=["*.env"]
)
per_run = asyncio.run(toolset.for_run(MagicMock())) # noqa: spec (for_run ignores ctx)
assert per_run is not toolset
assert per_run._exclude_tools == {"run_skill_script"}
assert per_run._exclude_resources == ["*.env"]


class TestCleanup:
def test_cleanup_runs_if_inner_construction_fails(self):
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,12 @@ apache-airflow-task-sdk-integration-tests = false

# Manual overrides (kept outside the auto-generated block above so the
# update_airflow_pyproject_toml.py script doesn't clobber them).
# Temporary: pydantic-ai-skills 1.2.0 (adds exclude_resources, required by the
# apache-airflow-providers-common-ai[skills] floor) was published 2026-07-15,
# inside the "4 days" exclude-newer window. This pins the per-package cutoff just
# past that release so 1.2.0 resolves while any later release stays quarantined.
# Remove once the rolling window advances past 2026-07-15.
pydantic-ai-skills = "2026-07-16T00:00:00Z"

[tool.uv.pip]
# Synchroonize with scripts/ci/prek/upgrade_important_versions.py
Expand Down Expand Up @@ -1706,6 +1712,8 @@ apache-airflow-task-sdk-integration-tests = false

# Manual overrides — see the matching block under
# `[tool.uv.exclude-newer-package]` above for rationale.
# Temporary: see the pydantic-ai-skills note in `[tool.uv.exclude-newer-package]`.
pydantic-ai-skills = "2026-07-16T00:00:00Z"

[tool.uv.sources]
# These names must match the names as defined in the pyproject.toml of the workspace items,
Expand Down
Loading
Loading