diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index 9e11ecbe96..e6213978b9 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -107,6 +107,15 @@ def __init__( self.content = content self.function = function + # Precompute whether the function accepts **kwargs to avoid + # repeated inspect.signature() calls on every invocation. + self._accepts_kwargs: bool = False + if function is not None: + sig = inspect.signature(function) + self._accepts_kwargs = any( + p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() + ) + class Skill: """A skill definition with optional resources. @@ -510,7 +519,7 @@ def _load_skill(self, skill_name: str) -> str: return content - async def _read_skill_resource(self, skill_name: str, resource_name: str) -> str: + async def _read_skill_resource(self, skill_name: str, resource_name: str, **kwargs: Any) -> str: """Read a named resource from a skill. Resolves the resource by case-insensitive name lookup. Static @@ -520,6 +529,9 @@ async def _read_skill_resource(self, skill_name: str, resource_name: str) -> str Args: skill_name: The name of the owning skill. resource_name: The resource name to look up (case-insensitive). + **kwargs: Runtime keyword arguments forwarded to resource functions + that accept ``**kwargs`` (e.g. arguments passed via + ``agent.run(user_id="123")``). Returns: The resource content string, or a user-facing error message on @@ -549,9 +561,11 @@ async def _read_skill_resource(self, skill_name: str, resource_name: str) -> str if resource.function is not None: try: if inspect.iscoroutinefunction(resource.function): - result = await resource.function() + result = ( + await resource.function(**kwargs) if resource._accepts_kwargs else await resource.function() + ) else: - result = resource.function() + result = resource.function(**kwargs) if resource._accepts_kwargs else resource.function() return str(result) except Exception as exc: logger.exception("Failed to read resource '%s' from skill '%s'", resource_name, skill_name) diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index c572f4727b..b80581a50d 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -6,6 +6,7 @@ import os from pathlib import Path +from typing import Any from unittest.mock import AsyncMock import pytest @@ -993,6 +994,42 @@ async def test_read_unknown_resource_returns_error(self) -> None: result = await provider._read_skill_resource("prog-skill", "nonexistent") assert result.startswith("Error:") + async def test_read_callable_resource_sync_with_kwargs(self) -> None: + skill = Skill(name="prog-skill", description="A skill.", content="Body") + + @skill.resource + def get_user_config(**kwargs: Any) -> str: + user_id = kwargs.get("user_id", "unknown") + return f"config for {user_id}" + + provider = SkillsProvider(skills=[skill]) + result = await provider._read_skill_resource("prog-skill", "get_user_config", user_id="user_123") + assert result == "config for user_123" + + async def test_read_callable_resource_async_with_kwargs(self) -> None: + skill = Skill(name="prog-skill", description="A skill.", content="Body") + + @skill.resource + async def get_user_data(**kwargs: Any) -> str: + token = kwargs.get("auth_token", "none") + return f"data with token={token}" + + provider = SkillsProvider(skills=[skill]) + result = await provider._read_skill_resource("prog-skill", "get_user_data", auth_token="abc") + assert result == "data with token=abc" + + async def test_read_callable_resource_without_kwargs_ignores_extra_args(self) -> None: + """Resource functions without **kwargs should still work when kwargs are passed.""" + skill = Skill(name="prog-skill", description="A skill.", content="Body") + + @skill.resource + def static_resource() -> str: + return "static content" + + provider = SkillsProvider(skills=[skill]) + result = await provider._read_skill_resource("prog-skill", "static_resource", user_id="ignored") + assert result == "static content" + async def test_before_run_injects_code_skills(self) -> None: skill = Skill(name="prog-skill", description="A code-defined skill.", content="Body") provider = SkillsProvider(skills=[skill]) diff --git a/python/samples/02-agents/skills/code_skill/README.md b/python/samples/02-agents/skills/code_skill/README.md index 828e7c8e22..4900d00eb5 100644 --- a/python/samples/02-agents/skills/code_skill/README.md +++ b/python/samples/02-agents/skills/code_skill/README.md @@ -4,12 +4,13 @@ This sample demonstrates how to create **Agent Skills** in Python code, without ## What are Code-Defined Skills? -While file-based skills use `SKILL.md` files discovered on disk, code-defined skills let you define skills entirely in Python using `Skill` and `SkillResource` classes. Two patterns are shown: +While file-based skills use `SKILL.md` files discovered on disk, code-defined skills let you define skills entirely in Python using `Skill` and `SkillResource` classes. Three patterns are shown: 1. **Basic Code Skill** — Create a `Skill` directly with static resources (inline content) 2. **Dynamic Resources** — Attach callable resources via the `@skill.resource` decorator that generate content at invocation time +3. **Dynamic Resources with kwargs** — Attach a callable resource that accepts `**kwargs` to receive runtime arguments passed via `agent.run()`, useful for injecting request-scoped context (user tokens, session data) -Both patterns can be combined with file-based skills in a single `SkillsProvider`. +All patterns can be combined with file-based skills in a single `SkillsProvider`. ## Project Structure @@ -47,7 +48,7 @@ uv run samples/02-agents/skills/code_skill/code_skill.py The sample runs two examples: 1. **Code style question** — Uses Pattern 1 (static resources): the agent loads the `code-style` skill and reads the `style-guide` resource to answer naming convention questions -2. **Project info question** — Uses Pattern 2 (dynamic resources): the agent reads dynamically generated `environment` and `team-roster` resources +2. **Project info question** — Uses Patterns 2 & 3 (dynamic resources with kwargs): the agent reads the dynamically generated `team-roster` resource and the `environment` resource which receives `app_version` via runtime kwargs ## Learn More diff --git a/python/samples/02-agents/skills/code_skill/code_skill.py b/python/samples/02-agents/skills/code_skill/code_skill.py index 3c95688c49..e111567244 100644 --- a/python/samples/02-agents/skills/code_skill/code_skill.py +++ b/python/samples/02-agents/skills/code_skill/code_skill.py @@ -4,6 +4,7 @@ import os import sys from textwrap import dedent +from typing import Any from agent_framework import Agent, Skill, SkillResource, SkillsProvider from agent_framework.azure import AzureOpenAIResponsesClient @@ -14,7 +15,7 @@ Code-Defined Agent Skills — Define skills in Python code This sample demonstrates how to create Agent Skills in code, -without needing SKILL.md files on disk. Two patterns are shown: +without needing SKILL.md files on disk. Three patterns are shown: Pattern 1: Basic Code Skill Create a Skill instance directly with static resources (inline content). @@ -24,6 +25,11 @@ decorator. Resources can be sync or async functions that generate content at invocation time. +Pattern 3: Dynamic Resources with kwargs + Attach a callable resource that accepts **kwargs to receive runtime + arguments passed via agent.run(). This is useful for injecting + request-scoped context (user tokens, session data) into skill resources. + Both patterns can be combined with file-based skills in a single SkillsProvider. """ @@ -72,12 +78,15 @@ @project_info_skill.resource -def environment() -> str: +def environment(**kwargs: Any) -> str: """Get current environment configuration.""" + # Access runtime kwargs passed via agent.run(app_version="...") + app_version = kwargs.get("app_version", "unknown") env = os.environ.get("APP_ENV", "development") region = os.environ.get("APP_REGION", "us-east-1") return f"""\ # Environment Configuration + - App Version: {app_version} - Environment: {env} - Region: {region} - Python: {sys.version} @@ -124,10 +133,11 @@ async def main() -> None: response = await agent.run("What naming convention should I use for class attributes?") print(f"Agent: {response}\n") - # Example 2: Project info question (Pattern 2 — dynamic resources) + # Example 2: Project info question (Pattern 2 & 3 — dynamic resources with kwargs) print("Example 2: Project info question") print("---------------------------------") - response = await agent.run("What environment are we running in and who is on the team?") + # Pass app_version as a runtime kwarg; it flows to the environment() resource via **kwargs + response = await agent.run("What environment are we running in and who is on the team?", app_version="2.4.1") print(f"Agent: {response}\n") """ @@ -141,9 +151,9 @@ async def main() -> None: Example 2: Project info question --------------------------------- - Agent: We're running in the development environment in us-east-1. - The team consists of Alice Chen (Tech Lead), Bob Smith (Backend Engineer), - and Carol Davis (Frontend Engineer). + Agent: We're running app version 2.4.1 in the development environment + in us-east-1. The team consists of Alice Chen (Tech Lead), Bob Smith + (Backend Engineer), and Carol Davis (Frontend Engineer). """