Skip to content
Merged
20 changes: 17 additions & 3 deletions python/packages/core/agent_framework/_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions python/packages/core/tests/core/test_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import os
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock

import pytest
Expand Down Expand Up @@ -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])
Expand Down
7 changes: 4 additions & 3 deletions python/samples/02-agents/skills/code_skill/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
24 changes: 17 additions & 7 deletions python/samples/02-agents/skills/code_skill/code_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
Expand All @@ -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.
"""

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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")

"""
Expand All @@ -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).
"""


Expand Down