Advanced Features
Advanced patterns and features for sophisticated skill systems.
Skill Decorator Pattern
@toolset.skill() Decorator
The @toolset.skill() decorator enables defining skills directly on a SkillsToolset instance. This approach is ideal when:
- Skills are tightly coupled with your agent initialization
- You want to define skills inline without separate files
- Skills depend on runtime dependencies available in your agent
Basic Example
from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.skills import SkillsToolset
skills = SkillsToolset()
@skills.skill()
def data_analyzer() -> str:
"""Analyze data from various sources."""
return """
# Data Analysis Skill
Use this skill to analyze datasets, generate statistical insights, and create visualizations.
## Instructions
1. Load the skill with `load_skill`
2. Access available resources with `read_skill_resource`
3. Execute analysis scripts with `run_skill_script`
## Key Capabilities
- Statistical summaries and distributions
- Correlation and trend analysis
- Data transformation and aggregation
- Report generation
"""
agent = Agent(
model='openai:gpt-4o',
toolsets=[skills]
)
result = agent.run_sync('Analyze the quarterly sales data')
Decorator Parameters
@toolset.skill(
name='custom-name', # Override function name (default: normalize_skill_name(func_name))
description='...', # Override docstring-based description
license='Apache-2.0', # License information
compatibility='Python 3.10+', # Environment requirements
metadata={'version': '1.0'}, # Custom metadata fields
resources=[...], # Initial resources
scripts=[...] # Initial scripts
)
def my_skill() -> str:
return "..."
Parameters Explained
| Parameter | Type | Required | Notes |
|---|---|---|---|
name |
str |
No | Defaults to function name with underscores converted to hyphens. Must match pattern ^[a-z0-9]+(-[a-z0-9]+)*$ and be ≤64 chars. |
description |
str |
No | Extracted from function docstring if not provided. Max 1024 characters. |
license |
str |
No | License identifier (e.g., "Apache-2.0", "MIT"). Included in skill metadata. |
compatibility |
str |
No | Environment/dependency requirements (e.g., "Requires git, docker, internet access"). Max 500 chars. |
metadata |
dict |
No | Custom key-value pairs preserved in skill metadata. Useful for versioning or custom properties. |
resources |
list[SkillResource] |
No | Initial resources attached to the skill. Can be extended with @skill.resource. |
scripts |
list[SkillScript] |
No | Initial scripts attached to the skill. Can be extended with @skill.script. |
Adding Resources and Scripts
The decorator returns a SkillWrapper that allows further decorating resources and scripts:
@skills.skill()
def data_analyzer() -> str:
return "Analyze data from various sources..."
# Add dynamic resources
@data_analyzer.resource
def get_schema() -> str:
"""Get the data schema."""
return "## Schema\n\nColumns: id, name, value, timestamp"
@data_analyzer.resource
async def get_available_tables(ctx: RunContext[MyDeps]) -> str:
"""Get available database tables (with runtime context)."""
tables = await ctx.deps.database.list_tables()
return f"Available tables:\n" + "\n".join(tables)
# Add executable scripts
@data_analyzer.script
async def analyze_query(ctx: RunContext[MyDeps], query: str) -> str:
"""Execute an analysis query."""
result = await ctx.deps.database.execute(query)
return str(result)
How Skill Names Are Derived
The decorator automatically converts function names to valid skill names:
@skills.skill()
def my_data_tool() -> str:
# Skill name: "my-data-tool"
return "..."
@skills.skill()
def MySkill() -> str:
# Skill name: "myskill" (lowercased, hyphens added between camelCase)
return "..."
@skills.skill(name='custom-name')
def function_with_explicit_name() -> str:
# Skill name: "custom-name" (uses provided name)
return "..."
Custom Instruction Templates
Customizing Agent Instructions
By default, SkillsToolset injects a standard instruction template that explains the progressive disclosure pattern. You can customize this for your specific use case:
from pydantic_ai import Agent
from pydantic_ai.toolsets.skills import SkillsToolset
custom_template = """\
You have access to specialized skills for domain-specific knowledge.
## How to Use Skills
Each skill contains:
- **Instructions**: Guidelines on when and how to use the skill
- **Resources**: Reference documentation and data
- **Scripts**: Executable functions for specific tasks
Always follow this workflow:
1. Call `list_skills` to see available skills
2. Call `load_skill` to understand the skill's capabilities
3. Use `read_skill_resource` for specific reference material
4. Execute scripts with `run_skill_script` when needed
## Available Skills
{skills_list}
## Tips
- Load skills only when relevant to the user's request
- Consult resources before calling scripts
- Check skill descriptions to understand their scope
"""
toolset = SkillsToolset(
directories=['./skills'],
instruction_template=custom_template
)
agent = Agent(
model='openai:gpt-4o',
toolsets=[toolset]
)
Template Variables
Your custom template must include the {skills_list} placeholder, which is automatically replaced with formatted skill information:
{skills_list} # Replaced with XML-formatted list of available skills
The skills list includes: - Skill name, description, and URI - List of available resources - List of available scripts - Full skill instructions
Default Template
If you don't provide a custom template, the default is used:
DEFAULT_INSTRUCTION_TEMPLATE = """\
Here is a list of skills that contain domain specific knowledge on a variety of topics.
Each skill comes with a description of the topic and instructions on how to use it.
When a user asks you to perform a task that falls within the domain of a skill, use the `load_skill` tool to acquire the full instructions.
<available_skills>
{skills_list}
</available_skills>
Use progressive disclosure: load only what you need, when you need it:
- First, use `load_skill` tool to read the full skill instructions
- To read additional resources within a skill, use `read_skill_resource` tool
- To execute skill scripts, use `run_skill_script` tool
"""
Dependency Injection via RunContext
Using RunContext in Resources and Scripts
Both @skill.resource and @skill.script decorated functions can optionally accept a RunContext[DepsType] parameter to access dependencies. The toolset automatically detects this by inspecting the function signature.
Example: Database Access
from typing import TypedDict
from pydantic_ai import RunContext
from pydantic_ai.toolsets.skills import SkillsToolset
class MyDeps(TypedDict):
"""Dependencies available in RunContext."""
database: Database
cache: Cache
logger: Logger
skills = SkillsToolset()
@skills.skill()
def database_skill() -> str:
return "Query and analyze database content"
# Resource with context - accesses the database dependency
@database_skill.resource
async def get_current_schema(ctx: RunContext[MyDeps]) -> str:
"""Fetch current schema from the database."""
schema = await ctx.deps.database.get_schema()
return f"## Current Schema\n{schema}"
# Script with context - executes queries with database access
@database_skill.script
async def execute_query(
ctx: RunContext[MyDeps],
query: str,
limit: int = 100
) -> str:
"""Execute a SQL query against the database."""
result = await ctx.deps.database.query(query, limit=limit)
# Log the execution
await ctx.deps.logger.info(f"Executed query: {query[:50]}...")
# Cache the result
cache_key = f"query:{query}"
await ctx.deps.cache.set(cache_key, result, ttl=3600)
return str(result)
Type-Safe Dependency Access
The RunContext is generic over your dependency type, enabling type-safe access:
# When passing dependencies to your agent
from pydantic_ai import Agent
agent = Agent(
model='openai:gpt-4o',
deps=MyDeps(
database=my_database,
cache=my_cache,
logger=my_logger
),
toolsets=[skills]
)
result = agent.run_sync('Query the user database', deps=agent.deps)
Signature Detection
The decorator automatically determines if a function takes context:
# ✓ Automatically detected as needing context
@skill.resource
async def needs_context(ctx: RunContext[MyDeps]) -> str:
return str(ctx.deps)
# ✓ Correctly detected as not needing context
@skill.resource
def static_resource() -> str:
return "Static content"
# ✓ Works with sync functions too
@skill.script
def sync_script(ctx: RunContext[MyDeps]) -> str:
return str(ctx.deps)
Custom Script Executors
Creating Custom Executors
For advanced use cases, you can provide custom script executors that handle script execution differently than the built-in local executor. This enables:
- Remote execution (cloud functions, etc.)
- Sandboxing or containerized execution
- Custom security policies
- Integration with external systems
Executor Interface
Custom executors must implement the SkillScriptExecutor protocol:
from typing import Protocol, Any
class SkillScriptExecutor(Protocol):
"""Protocol for custom script execution."""
async def execute(
self,
script: SkillScript,
args: dict[str, Any] | None = None
) -> str:
"""Execute a skill script.
Args:
script: The script to execute
args: Optional arguments to pass to the script
Returns:
String output from the script
"""
...
Example: Remote Execution
from pydantic_ai.toolsets.skills import SkillScript, CallableSkillScriptExecutor
import httpx
class RemoteExecutor:
"""Execute scripts on a remote server."""
def __init__(self, server_url: str):
self.server_url = server_url
async def execute(self, script: SkillScript, args: dict | None = None) -> str:
"""Send script execution request to remote server."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.server_url}/execute",
json={
"script_name": script.name,
"args": args or {}
}
)
return response.text
# Use in custom script definitions
from pydantic_ai.toolsets.skills import SkillScript
remote_executor = RemoteExecutor("https://api.example.com")
script = SkillScript(
name='remote-analysis',
description='Run analysis on remote server',
function=None,
executor=remote_executor # Custom executor
)
Example: Sandboxed Execution
import subprocess
from pathlib import Path
class SandboxedExecutor:
"""Execute Python scripts in isolated sandboxes."""
def __init__(self, timeout: int = 30):
self.timeout = timeout
async def execute(self, script: SkillScript, args: dict | None = None) -> str:
"""Execute script in sandbox."""
# Convert args to environment variables
env = {f"ARG_{k.upper()}": str(v) for k, v in (args or {}).items()}
try:
result = subprocess.run(
["sandbox", "--timeout", str(self.timeout), script.name],
capture_output=True,
text=True,
env={**os.environ, **env},
timeout=self.timeout + 5
)
return result.stdout or result.stderr
except subprocess.TimeoutExpired:
return f"ERROR: Script execution timed out after {self.timeout}s"
Metadata Management
Custom Metadata Fields
You can attach arbitrary metadata to skills for custom use cases:
@skills.skill(
metadata={
'version': '1.0.0',
'author': 'data-team',
'tags': ['analytics', 'database', 'reporting'],
'supported_formats': ['csv', 'json', 'parquet'],
'min_pydantic_ai_version': '0.1.0'
}
)
def advanced_analytics() -> str:
return "Advanced analytics skill..."
Accessing Metadata
Metadata is preserved in the Skill object and accessible via:
skill = toolset.get_skill('advanced-analytics')
# Access via skill attributes
if hasattr(skill, 'metadata') and skill.metadata:
version = skill.metadata.get('version', '0.0.0')
tags = skill.metadata.get('tags', [])
Recommended Metadata Fields
Common metadata patterns:
| Field | Type | Purpose |
|---|---|---|
version |
str |
Semantic version for the skill |
author |
str |
Team or person who maintains the skill |
tags |
list[str] |
Categorization tags for discovery |
requires |
dict[str, str] |
External dependencies and versions |
deprecated |
bool |
Mark skills as deprecated |
deprecation_message |
str |
Explanation and alternative if deprecated |
breaking_changes |
str |
Document breaking changes in versions |
supported_models |
list[str] |
Models this skill works best with |
Mixed Skill Scenarios
Combining Programmatic and File-Based Skills
The real power comes when combining both approaches:
from pydantic_ai import Agent
from pydantic_ai.toolsets.skills import SkillsToolset, Skill
# Create programmatic skills
skills = SkillsToolset(
directories=['./skills', './custom-skills'], # File-based skills
max_depth=3 # Limit discovery depth
)
# Add decorator-defined skills to the same toolset
@skills.skill()
def runtime_analyzer() -> str:
return "Analyze runtime metrics and performance data"
@runtime_analyzer.resource
async def get_metrics(ctx: RunContext[MyDeps]) -> str:
metrics = await ctx.deps.monitoring.get_current()
return f"Current metrics:\n{metrics}"
# Now agent has access to both:
# - File-based skills from ./skills and ./custom-skills
# - Programmatically defined runtime_analyzer skill
agent = Agent(
model='openai:gpt-4o',
toolsets=[skills]
)
Skill Precedence and Conflicts
When multiple skills have the same name:
- Programmatic skills (defined via
@decorator) take precedence - File-based skills are loaded in directory order
- Duplicate detection: If a duplicate is detected, a warning is issued and the new skill is registered
# This will warn if a skill named 'data-analyzer' exists in ./skills
@skills.skill(name='data-analyzer')
def conflicting_skill() -> str:
return "..."
Dynamic Skill Registration
You can programmatically register skills after toolset creation (though typically skills are defined at initialization):
from pydantic_ai.toolsets.skills import Skill
# Create toolset
toolset = SkillsToolset()
# Register a new programmatic skill
new_skill = Skill(
name='dynamically-added',
description='Added after initialization',
content='This skill was registered dynamically'
)
toolset._register_skill(new_skill) # Internal method, use with caution
Note: Direct manipulation of internal
_register_skill()is not recommended for production code. Define skills at initialization time for better clarity and maintainability.
See Also
- Programmatic Skills - Detailed guide to programmatic skill creation
- API Reference - SkillsToolset - Complete API documentation
- Implementation Patterns - Common design patterns and best practices