chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# Agent Skills Samples
|
||||
|
||||
These samples demonstrate how to use **Agent Skills** — modular packages of instructions, resources, and scripts that extend an agent's capabilities. Skills follow the [Agent Skills specification](https://agentskills.io/) and use progressive disclosure to optimize token usage.
|
||||
|
||||
## Learning Path
|
||||
|
||||
Start with file-based or code-defined skills, then explore combining them and adding approval workflows.
|
||||
|
||||
| Sample | Description |
|
||||
|--------|-------------|
|
||||
| [**file_based_skill**](file_based_skill/) | Define skills as `SKILL.md` files on disk with reference documents and executable scripts. Uses the unit-converter skill. |
|
||||
| [**code_defined_skill**](code_defined_skill/) | Define skills entirely in Python code using `Skill`, `@skill.resource`, and `@skill.script` decorators. Uses a code-defined unit-converter skill. |
|
||||
| [**class_based_skill**](class_based_skill/) | Define skills as Python classes using `ClassSkill` with `@ClassSkill.resource` and `@ClassSkill.script` decorators for auto-discovery. Uses a class-based unit-converter skill. |
|
||||
| [**mixed_skills**](mixed_skills/) | Combine code-defined, class-based, and file-based skills in a single agent. Uses a code-defined volume-converter, a class-based temperature-converter, and a file-based unit-converter. |
|
||||
| [**mcp_based_skill**](mcp_based_skill/) | Discover skills served over the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) via `MCPSkillsSource`. Connects to a remote MCP server that exposes skills as `skill://...` resources following the SEP-2640 convention. |
|
||||
| [**script_approval**](script_approval/) | Require manual human-in-the-loop approval before running skill tools (the default). |
|
||||
| [**skills_auto_approval**](skills_auto_approval/) | Configure auto-approval rules with `ToolApprovalMiddleware` so read-only skill tools are approved automatically while script execution still prompts. |
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
Skills use a three-step interaction model to minimize token usage:
|
||||
|
||||
1. **Advertise** — Skill names and descriptions (~100 tokens each) are injected into the system prompt
|
||||
2. **Load** — Full instructions are loaded on-demand via the `load_skill` tool
|
||||
3. **Access** — Resources are read via `read_skill_resource`; scripts are executed via `run_skill_script`
|
||||
|
||||
### File-Based vs Code-Defined vs Class-Based Skills
|
||||
|
||||
| Aspect | File-Based | Code-Defined | Class-Based |
|
||||
|--------|-----------|--------------|-------------|
|
||||
| Definition | `SKILL.md` files on disk | `Skill` instances in Python | Classes extending `ClassSkill` |
|
||||
| Resources | Static files in `references/` and `assets/` directories | Callable functions via `@skill.resource` decorator | `@ClassSkill.resource` decorator (auto-discovered) |
|
||||
| Scripts | Python files in `scripts/` directory (executed via subprocess) | Callable functions via `@skill.script` decorator (executed in-process) | `@ClassSkill.script` decorator (executed in-process) |
|
||||
| Discovery | Automatic via `skill_paths` parameter | Explicit via `skills` parameter | Explicit via `skills` parameter |
|
||||
| Dynamic content | No (static files only) | Yes (functions can generate content at runtime) | Yes (functions can generate content at runtime) |
|
||||
| Sharing pattern | Copy skill directory | Inline or shared instances | Package in shared libraries/PyPI |
|
||||
|
||||
All three types can be combined in a single `SkillsProvider` — see the [mixed_skills](mixed_skills/) sample.
|
||||
|
||||
### Script Execution
|
||||
|
||||
Skills can include executable scripts. How a script runs depends on how it was defined:
|
||||
|
||||
| | Code-Defined Scripts | File-Based Scripts |
|
||||
|---|---|---|
|
||||
| **Defined via** | `@skill.script` decorator | `.py` files in `scripts/` directory |
|
||||
| **Execution** | In-process (direct function call) | Delegated to a `script_runner` |
|
||||
| **`script_runner` needed?** | No — runs in-process automatically | **Yes** — required |
|
||||
|
||||
The `script_runner` parameter on `SkillsProvider` is only applicable to **file-based** scripts. Code-defined scripts are always executed in-process regardless of this setting. See [file_based_skill](file_based_skill/) for an example using a `SkillScriptRunner` callable with a subprocess runner, and [code_defined_skill](code_defined_skill/) for in-process scripts that need no runner.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
All samples require:
|
||||
- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model (e.g. `gpt-4o-mini`)
|
||||
- Azure CLI authentication (`az login`)
|
||||
- Environment variables set in a `.env` file (see `python/.env.example`)
|
||||
|
||||
## Suppressing the experimental MCP Skills warning
|
||||
|
||||
The core Agent Skills APIs are stable. MCP-based skill discovery
|
||||
(`MCPSkillsSource`) is still experimental, so the [mcp_based_skill](mcp_based_skill/)
|
||||
sample includes a short commented `warnings.filterwarnings(...)` snippet near the
|
||||
imports. Uncomment it if you want to suppress the MCP Skills warning.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Class-Based Agent Skills
|
||||
|
||||
This sample demonstrates how to define **Agent Skills as Python classes** using `ClassSkill`.
|
||||
|
||||
## What's Demonstrated
|
||||
|
||||
- Creating skills as classes that extend `ClassSkill`
|
||||
- Bundling name, description, instructions, resources, and scripts into a single class
|
||||
- Using `@ClassSkill.resource` decorator for automatic resource discovery
|
||||
- Using `@ClassSkill.script` decorator for automatic script discovery
|
||||
- Lazy-loading and caching of resources and scripts
|
||||
- Registering class-based skills with `SkillsProvider`
|
||||
|
||||
## Skills Included
|
||||
|
||||
### unit-converter (class-based)
|
||||
|
||||
A `UnitConverterSkill` class that converts between common units. Defined in `class_based_skill.py`:
|
||||
|
||||
- `conversion-table` — Static resource with factor table
|
||||
- `convert` — Script that performs `value × factor` conversion
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
class_based_skill/
|
||||
├── class_based_skill.py
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model (e.g. `gpt-4o-mini`)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set the required environment variables in a `.env` file (see `python/.env.example`):
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_MODEL`: The name of your model deployment (defaults to `gpt-4o-mini`)
|
||||
|
||||
### Authentication
|
||||
|
||||
This sample uses `AzureCliCredential` for authentication. Run `az login` in your terminal before running the sample.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run samples/02-agents/skills/class_based_skill/class_based_skill.py
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
|
||||
```
|
||||
Converting units with class-based skills
|
||||
------------------------------------------------------------
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. **26.2 miles → 42.16 km** (a marathon distance)
|
||||
2. **75 kg → 165.35 lbs**
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
- [Code-Defined Skills Sample](../code_defined_skill/)
|
||||
- [Mixed Skills Sample](../mixed_skills/)
|
||||
- [Microsoft Agent Framework Documentation](../../../../../docs/)
|
||||
@@ -0,0 +1,148 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from textwrap import dedent
|
||||
|
||||
from agent_framework import Agent, ClassSkill, SkillFrontmatter, SkillsProvider, ToolApprovalMiddleware
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""
|
||||
Class-Based Agent Skills — Define skills as Python classes
|
||||
|
||||
This sample demonstrates how to define Agent Skills as reusable Python classes
|
||||
by subclassing ``ClassSkill``. Class-based skills bundle all components (name,
|
||||
description, instructions, resources, scripts) into a single class, making
|
||||
them easy to package and distribute via shared libraries or PyPI.
|
||||
|
||||
Key concepts shown:
|
||||
- Subclassing ``ClassSkill`` to create a self-contained skill
|
||||
- Using ``@property`` + ``@ClassSkill.resource`` (bare) — name defaults to method name
|
||||
- Using ``@ClassSkill.script(name=..., description=...)`` — explicit name and description
|
||||
- Lazy-loading and caching of resources and scripts
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Class-Based Skill: UnitConverterSkill
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class UnitConverterSkill(ClassSkill):
|
||||
"""A unit-converter skill defined as a Python class.
|
||||
|
||||
Converts between common units (miles↔km, pounds↔kg) using a
|
||||
conversion factor. Resources and scripts are discovered automatically
|
||||
via decorators.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
frontmatter=SkillFrontmatter(
|
||||
name="unit-converter",
|
||||
description=(
|
||||
"Convert between common units using a multiplication factor. "
|
||||
"Use when asked to convert miles, kilometers, pounds, or kilograms."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def instructions(self) -> str:
|
||||
return dedent("""\
|
||||
Use this skill when the user asks to convert between units.
|
||||
|
||||
1. Review the conversion-table resource to find the factor for the requested conversion.
|
||||
2. Use the convert script, passing the value and factor from the table.
|
||||
3. Present the result clearly with both units.
|
||||
""")
|
||||
|
||||
# 1. Property with bare decorator — name defaults to the method name
|
||||
# ("conversion_table" → "conversion-table"), no description.
|
||||
# Place @property first, then @ClassSkill.resource.
|
||||
@property
|
||||
@ClassSkill.resource
|
||||
def conversion_table(self) -> str:
|
||||
"""Lookup table of multiplication factors for common unit conversions."""
|
||||
return dedent("""\
|
||||
# Conversion Tables
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
""")
|
||||
|
||||
# 2. Explicit name — overrides the method name
|
||||
# 3. Explicit description — provides a description for the script
|
||||
@ClassSkill.script(name="convert", description="Multiplies a value by a conversion factor.")
|
||||
def convert_units(self, value: float, factor: float) -> str:
|
||||
"""Convert a value using a multiplication factor: result = value × factor.
|
||||
|
||||
Args:
|
||||
value: The numeric value to convert.
|
||||
factor: Conversion factor from the conversion table.
|
||||
|
||||
Returns:
|
||||
JSON string with the inputs and converted result.
|
||||
"""
|
||||
result = round(value * factor, 4)
|
||||
return json.dumps({"value": value, "factor": factor, "result": result})
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the class-based skills demo."""
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini")
|
||||
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=endpoint,
|
||||
model=deployment,
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Instantiate the class-based skill and pass it to the provider
|
||||
unit_converter = UnitConverterSkill()
|
||||
|
||||
# All skill tools require approval by default; auto-approve them so the
|
||||
# sample runs unattended. See the script_approval / skills_auto_approval
|
||||
# samples for interactive and selective approval handling.
|
||||
async with Agent(
|
||||
client=client,
|
||||
instructions="You are a helpful assistant that can convert units.",
|
||||
context_providers=[SkillsProvider(unit_converter)],
|
||||
middleware=[ToolApprovalMiddleware(auto_approval_rules=[SkillsProvider.all_tools_auto_approval_rule])],
|
||||
) as agent:
|
||||
print("Converting units with class-based skills")
|
||||
print("-" * 60)
|
||||
session = agent.create_session()
|
||||
response = await agent.run(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Agent: {response}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Sample output:
|
||||
|
||||
Converting units with class-based skills
|
||||
------------------------------------------------------------
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. **26.2 miles → 42.16 km** (a marathon distance)
|
||||
2. **75 kg → 165.35 lbs**
|
||||
"""
|
||||
@@ -0,0 +1,49 @@
|
||||
# Code-Defined Agent Skills
|
||||
|
||||
This sample demonstrates how to create **Agent Skills** in Python code, without needing `SKILL.md` files on disk. A unit-converter skill shows three approaches:
|
||||
|
||||
## What's Demonstrated
|
||||
|
||||
1. **Static Resources** — Pass inline content via the `resources` parameter when constructing a `Skill`
|
||||
2. **Dynamic Resources** — Attach callable functions via the `@skill.resource` decorator that return content computed at runtime
|
||||
3. **Dynamic Scripts** — Attach callable scripts via the `@skill.script` decorator (unit conversion via a single factor parameter)
|
||||
|
||||
All three can be combined with file-based skills in a single `SkillsProvider`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
code_defined_skill/
|
||||
├── code_defined_skill.py
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model (e.g. `gpt-4o-mini`)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set the required environment variables in a `.env` file (see `python/.env.example`):
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `AZURE_OPENAI_MODEL`: The name of your model deployment (defaults to `gpt-4o-mini`)
|
||||
|
||||
### Authentication
|
||||
|
||||
This sample uses `AzureCliCredential` for authentication. Run `az login` in your terminal before running the sample.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run samples/02-agents/skills/code_defined_skill/code_defined_skill.py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
- [File-Based Skills Sample](../file_based_skill/)
|
||||
- [Mixed Skills Sample](../mixed_skills/)
|
||||
- [Microsoft Agent Framework Documentation](../../../../../docs/)
|
||||
@@ -0,0 +1,184 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
InlineSkill,
|
||||
InlineSkillResource,
|
||||
SkillFrontmatter,
|
||||
SkillsProvider,
|
||||
ToolApprovalMiddleware,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""
|
||||
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. Three approaches are shown
|
||||
using a unit-converter skill:
|
||||
|
||||
1. Static Resources
|
||||
Pass inline content directly via the ``resources`` parameter when
|
||||
constructing the Skill.
|
||||
|
||||
2. Dynamic Resources
|
||||
Attach a callable resource via the @skill.resource decorator. The
|
||||
function is invoked on demand, so it can return data computed at
|
||||
runtime.
|
||||
|
||||
3. Dynamic Scripts
|
||||
Attach a callable script via the @skill.script decorator. Scripts are
|
||||
executable functions the agent can invoke directly in-process.
|
||||
|
||||
Code-defined skills can be combined with file-based skills in a single
|
||||
SkillsProvider — see the mixed_skills sample.
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Static Resources — inline content passed at construction time
|
||||
# ---------------------------------------------------------------------------
|
||||
unit_converter_skill = InlineSkill(
|
||||
frontmatter=SkillFrontmatter(
|
||||
name="unit-converter", description="Convert between common units using a conversion factor"
|
||||
),
|
||||
instructions=dedent("""\
|
||||
Use this skill when the user asks to convert between units.
|
||||
|
||||
1. Review the conversion-tables resource to find the factor for the
|
||||
requested conversion.
|
||||
2. Check the conversion-policy resource for rounding and formatting rules.
|
||||
3. Use the convert script, passing the value and factor from the table.
|
||||
"""),
|
||||
resources=[
|
||||
InlineSkillResource(
|
||||
name="conversion-tables",
|
||||
content=dedent("""\
|
||||
# Conversion Tables
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
"""),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Dynamic Resources — callable function via @skill.resource
|
||||
# ---------------------------------------------------------------------------
|
||||
@unit_converter_skill.resource(
|
||||
name="conversion-policy", description="Current conversion formatting and rounding policy"
|
||||
)
|
||||
def conversion_policy(**kwargs: Any) -> Any:
|
||||
"""Return the current conversion policy.
|
||||
|
||||
Dynamic resources are evaluated at runtime, so they can include
|
||||
live data such as dates, configuration values, or database lookups.
|
||||
|
||||
When the resource function accepts ``**kwargs``, runtime keyword
|
||||
arguments passed to ``agent.run()`` are forwarded automatically.
|
||||
|
||||
Args:
|
||||
**kwargs: Runtime keyword arguments from ``agent.run()``.
|
||||
For example, ``agent.run(..., function_invocation_kwargs={"precision": 2})``
|
||||
makes ``kwargs["precision"]`` available here.
|
||||
"""
|
||||
precision = kwargs.get("precision", 4)
|
||||
return dedent(f"""\
|
||||
# Conversion Policy
|
||||
|
||||
**Decimal places:** {precision}
|
||||
**Format:** Always show both the original and converted values with units
|
||||
""")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Dynamic Scripts — in-process callable function
|
||||
# ---------------------------------------------------------------------------
|
||||
@unit_converter_skill.script(name="convert", description="Convert a value: result = value × factor")
|
||||
def convert_units(value: float, factor: float, **kwargs: Any) -> str:
|
||||
"""Convert a value using a multiplication factor: result = value × factor.
|
||||
|
||||
The caller looks up the correct factor from the conversion-tables
|
||||
resource and passes it here.
|
||||
|
||||
Args:
|
||||
value: The numeric value to convert.
|
||||
factor: Conversion factor from the conversion table.
|
||||
**kwargs: Runtime keyword arguments from ``agent.run()``.
|
||||
The ``precision`` kwarg controls how many decimal places
|
||||
the result is rounded to (default 4).
|
||||
|
||||
Returns:
|
||||
JSON string with the inputs and converted result.
|
||||
"""
|
||||
precision = kwargs.get("precision", 4)
|
||||
result = round(value * factor, precision)
|
||||
return json.dumps({"value": value, "factor": factor, "result": result})
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the code-defined skills demo."""
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini")
|
||||
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=endpoint,
|
||||
model=deployment,
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Create the skills provider with the code-defined skill and pass it to the agent
|
||||
# All skill tools require approval by default; auto-approve them so the
|
||||
# sample runs unattended. See the script_approval / skills_auto_approval
|
||||
# samples for interactive and selective approval handling.
|
||||
async with Agent(
|
||||
client=client,
|
||||
instructions="You are a helpful assistant that can convert units.",
|
||||
context_providers=[SkillsProvider(unit_converter_skill)],
|
||||
middleware=[ToolApprovalMiddleware(auto_approval_rules=[SkillsProvider.all_tools_auto_approval_rule])],
|
||||
) as agent:
|
||||
print("Converting units")
|
||||
print("-" * 60)
|
||||
session = agent.create_session()
|
||||
response = await agent.run(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?",
|
||||
function_invocation_kwargs={"precision": 2},
|
||||
session=session,
|
||||
)
|
||||
print(f"Agent: {response}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Sample output:
|
||||
|
||||
Converting units
|
||||
------------------------------------------------------------
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. **26.2 miles → 42.16 km** (a marathon distance)
|
||||
2. **75 kg → 165.35 lbs**
|
||||
|
||||
I used the conversion factors from the reference table:
|
||||
miles × 1.60934 and kilograms × 2.20462.
|
||||
"""
|
||||
@@ -0,0 +1,69 @@
|
||||
# File-Based Agent Skills
|
||||
|
||||
This sample demonstrates how to use **file-based Agent Skills** with a `SkillsProvider` in the Microsoft Agent Framework. File-based skills are discovered from `SKILL.md` files on disk and can include reference documents and executable scripts.
|
||||
|
||||
## What are Agent Skills?
|
||||
|
||||
Agent Skills are modular packages of instructions and resources that enable AI agents to perform specialized tasks. They follow the [Agent Skills specification](https://agentskills.io/) and implement progressive disclosure:
|
||||
|
||||
1. **Advertise**: Skills are advertised with name + description (~100 tokens per skill)
|
||||
2. **Load**: Full instructions are loaded on-demand via `load_skill` tool
|
||||
3. **Resources**: References and other files loaded via `read_skill_resource` tool
|
||||
4. **Scripts**: Executable scripts run via `run_skill_script` tool
|
||||
|
||||
## Skills Included
|
||||
|
||||
### unit-converter
|
||||
Converts between common units (miles↔km, pounds↔kg) using a multiplication factor following [agentskills.io guidelines](https://agentskills.io/skill-creation/using-scripts).
|
||||
- `references/CONVERSION_TABLES.md` — Supported conversions and their factors
|
||||
- `scripts/convert.py` — Executable script with `--value` and `--factor` flags, JSON output, and `--help` support
|
||||
|
||||
## Key Components
|
||||
|
||||
- **`SkillsProvider`** — Discovers skills from `SKILL.md` files in a directory and registers tools for the agent
|
||||
- **`subprocess_script_runner`** — A `SkillScriptRunner` callback that runs scripts as local Python subprocesses, enabling the `run_skill_script` tool. Converts argument dicts to CLI flags (e.g. `{"value": 26.2, "factor": 1.60934}` → `--value 26.2 --factor 1.60934`). Shared across samples in [`../subprocess_script_runner.py`](../subprocess_script_runner.py).
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
file_based_skill/
|
||||
├── file_based_skill.py
|
||||
├── README.md
|
||||
└── skills/
|
||||
└── unit-converter/
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
│ └── CONVERSION_TABLES.md
|
||||
└── scripts/
|
||||
└── convert.py
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model (e.g. `gpt-4o-mini`)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set the required environment variables in a `.env` file (see `python/.env.example`):
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `AZURE_OPENAI_MODEL`: The name of your model deployment (defaults to `gpt-4o-mini`)
|
||||
|
||||
### Authentication
|
||||
|
||||
This sample uses `AzureCliCredential` for authentication. Run `az login` in your terminal before running the sample.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run samples/02-agents/skills/file_based_skill/file_based_skill.py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
- [Code-Defined Skills Sample](../code_defined_skill/)
|
||||
- [Mixed Skills Sample](../mixed_skills/)
|
||||
- [Microsoft Agent Framework Documentation](../../../../../docs/)
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import Agent, SkillsProvider, ToolApprovalMiddleware
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Add the skills folder root to sys.path so the shared subprocess_script_runner can be imported
|
||||
_SKILLS_ROOT = str(Path(__file__).resolve().parent.parent)
|
||||
if _SKILLS_ROOT not in sys.path:
|
||||
sys.path.insert(0, _SKILLS_ROOT)
|
||||
|
||||
from subprocess_script_runner import subprocess_script_runner # pyrefly: ignore[missing-import] # noqa: E402
|
||||
|
||||
"""
|
||||
File-Based Agent Skills
|
||||
|
||||
This sample demonstrates how to use file-based Agent Skills with a SkillsProvider.
|
||||
Agent Skills are modular packages of instructions and resources that extend an agent's
|
||||
capabilities. They follow progressive disclosure:
|
||||
|
||||
1. Advertise — skill names and descriptions are injected into the system prompt
|
||||
2. Load — full instructions are loaded on-demand via the load_skill tool
|
||||
3. Read resources — supplementary files are read via the read_skill_resource tool
|
||||
4. Run scripts — skill scripts are run via the run_skill_script tool
|
||||
|
||||
This sample includes the unit-converter skill which demonstrates all three
|
||||
file-based capabilities: instructions (SKILL.md), resources (CONVERSION_TABLES.md),
|
||||
and scripts (convert.py).
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the file-based skills demo."""
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini")
|
||||
|
||||
# Create the chat client
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=endpoint,
|
||||
model=deployment,
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Create the skills provider
|
||||
# Discovers skills from the 'skills' directory and configures the
|
||||
# subprocess_script_runner to run file-based scripts.
|
||||
skills_dir = Path(__file__).parent / "skills"
|
||||
skills_provider = SkillsProvider.from_paths(
|
||||
skill_paths=str(skills_dir),
|
||||
script_runner=subprocess_script_runner,
|
||||
)
|
||||
|
||||
# Create the agent with skills. All skill tools require approval by
|
||||
# default; auto-approve them so the sample runs unattended. See the
|
||||
# script_approval / skills_auto_approval samples for approval handling.
|
||||
async with Agent(
|
||||
client=client,
|
||||
instructions="You are a helpful assistant.",
|
||||
context_providers=[skills_provider],
|
||||
middleware=[ToolApprovalMiddleware(auto_approval_rules=[SkillsProvider.all_tools_auto_approval_rule])],
|
||||
) as agent:
|
||||
# The agent will: load the unit-converter skill, read the conversion
|
||||
# tables resource, then execute the convert.py script.
|
||||
print("Converting units")
|
||||
print("-" * 60)
|
||||
session = agent.create_session()
|
||||
response = await agent.run(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Agent: {response}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Sample output:
|
||||
|
||||
Converting units
|
||||
------------------------------------------------------------
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. **26.2 miles → 42.16 km** (a marathon distance)
|
||||
2. **75 kg → 165.35 lbs**
|
||||
|
||||
I used the conversion factors from the reference table:
|
||||
miles × 1.60934 and kilograms × 2.20462.
|
||||
"""
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: unit-converter
|
||||
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
|
||||
license: MIT
|
||||
compatibility: Works with any model that supports tool use.
|
||||
allowed-tools: convert
|
||||
metadata:
|
||||
author: agent-framework-samples
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
When the user requests a unit conversion:
|
||||
1. First, review `references/CONVERSION_TABLES.md` to find the correct factor
|
||||
2. Run the `scripts/convert.py` script with `--value <number> --factor <factor>` (e.g. `--value 26.2 --factor 1.60934`)
|
||||
3. Present the converted value clearly with both units
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# Conversion Tables
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Unit conversion script
|
||||
# Converts a value using a multiplication factor: result = value × factor
|
||||
#
|
||||
# Usage:
|
||||
# python scripts/convert.py --value 26.2 --factor 1.60934
|
||||
# python scripts/convert.py --value 75 --factor 2.20462
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert a value using a multiplication factor.",
|
||||
epilog="Examples:\n"
|
||||
" python scripts/convert.py --value 26.2 --factor 1.60934\n"
|
||||
" python scripts/convert.py --value 75 --factor 2.20462",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.")
|
||||
parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.")
|
||||
args = parser.parse_args()
|
||||
result = round(args.value * args.factor, 4)
|
||||
print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# MCP-Based Agent Skills Sample
|
||||
|
||||
This sample demonstrates how to discover **Agent Skills served over MCP** with an `Agent`.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- Connecting to a remote MCP server (over streamable HTTP) that exposes skill
|
||||
resources following the SEP-2640 convention.
|
||||
- Building a `SkillsProvider` from an `MCPSkillsSource`, which reads
|
||||
`skill://index.json` (SEP-2640 canonical discovery) and constructs skills from
|
||||
the index entries.
|
||||
- The progressive disclosure pattern across MCP: advertise → load → read
|
||||
resources, exactly as for filesystem-backed skills.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model
|
||||
- Azure CLI authentication (`az login`)
|
||||
- A running MCP server that hosts SEP-2640 skill resources (see "Providing
|
||||
an MCP server" below)
|
||||
|
||||
### Setup
|
||||
|
||||
Set the following environment variables (in a `.env` file or your shell):
|
||||
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://your-endpoint.services.ai.azure.com/api/projects/your-project"
|
||||
$env:FOUNDRY_MODEL="gpt-4o-mini"
|
||||
$env:MCP_SKILLS_SERVER_URL="https://your-mcp-server.example.com/mcp"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```powershell
|
||||
python mcp_based_skill.py
|
||||
```
|
||||
|
||||
## Providing an MCP server
|
||||
|
||||
This sample is a **consumer**: it does not host an MCP server itself. To try
|
||||
it end-to-end you need an MCP server that exposes the SEP-2640 skill
|
||||
resources (`skill://index.json` plus per-skill `SKILL.md`).
|
||||
|
||||
- See [`samples/02-agents/mcp/agent_as_mcp_server.py`](../../mcp/agent_as_mcp_server.py)
|
||||
for an example of hosting an MCP server via the Agent Framework.
|
||||
- The Model Context Protocol working group maintains reference MCP-skills
|
||||
servers at
|
||||
[`modelcontextprotocol/experimental-ext-skills`](https://github.com/modelcontextprotocol/experimental-ext-skills).
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Discovering skills over MCP means an *external* MCP server controls what skill content
|
||||
(including instructions and, for script-capable skills, the scripts the agent may run)
|
||||
reaches the agent. A compromised or untrustworthy server could return adversarial content
|
||||
designed to manipulate the agent (indirect prompt injection) or to exfiltrate data through
|
||||
skill instructions/scripts. This source is never enabled by default — connecting
|
||||
`MCPSkillsSource` to a server is an explicit opt-in. Only connect to MCP servers you have
|
||||
vetted and trust, and treat their responses as untrusted input.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
# Uncomment this filter to suppress the experimental MCP Skills warning before
|
||||
# using the sample's MCP Skills APIs.
|
||||
# import warnings
|
||||
# warnings.filterwarnings("ignore", message=r"\[MCP_SKILLS\].*", category=FutureWarning)
|
||||
from agent_framework import Agent, MCPSkillsSource, SkillsProvider, ToolApprovalMiddleware
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
"""
|
||||
MCP-Based Agent Skills
|
||||
|
||||
This sample demonstrates how to discover Agent Skills served over the
|
||||
Model Context Protocol (MCP) using :class:`MCPSkillsSource`.
|
||||
|
||||
The sample connects to a remote MCP server that exposes skill resources
|
||||
under the ``skill://`` URI scheme:
|
||||
|
||||
* ``skill://index.json`` — discovery document listing all skills
|
||||
* ``skill://<skill-name>/SKILL.md`` — the skill instructions
|
||||
|
||||
To run, set ``MCP_SKILLS_SERVER_URL`` to the streamable HTTP endpoint of an
|
||||
MCP server that hosts the skill resources.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Connect to a remote MCP skills server and run the agent."""
|
||||
load_dotenv()
|
||||
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini")
|
||||
mcp_url = os.environ["MCP_SKILLS_SERVER_URL"]
|
||||
|
||||
print("Discovering MCP-based skills")
|
||||
print("-" * 60)
|
||||
|
||||
# 1. Connect to the MCP server over streamable HTTP.
|
||||
async with streamable_http_client(url=mcp_url) as (read, write, _), ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
|
||||
# 2. Build a SkillsProvider that discovers skills over MCP.
|
||||
# MCPSkillsSource reads skill://index.json and creates one
|
||||
# MCPSkill per skill-md entry; SKILL.md bodies are fetched
|
||||
# on demand via resources/read.
|
||||
skills_provider = SkillsProvider(MCPSkillsSource(client=session))
|
||||
|
||||
# 3. Run the agent.
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=endpoint,
|
||||
model=deployment,
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
async with Agent(
|
||||
client=client,
|
||||
instructions="You are a helpful assistant. Use available skills to answer the user.",
|
||||
context_providers=[skills_provider],
|
||||
middleware=[ToolApprovalMiddleware(auto_approval_rules=[SkillsProvider.all_tools_auto_approval_rule])],
|
||||
) as agent:
|
||||
query = input("User: ").strip() # noqa: ASYNC250
|
||||
if not query:
|
||||
return
|
||||
session = agent.create_session()
|
||||
response = await agent.run(query, session=session)
|
||||
print(f"Agent: {response}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,118 @@
|
||||
# Mixed Skills — Code, Class, and File Skills
|
||||
|
||||
This sample demonstrates how to combine **code-defined skills**,
|
||||
**class-based skills**, and **file-based skills** in a single agent using
|
||||
`SkillsProvider`.
|
||||
|
||||
## Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Code skill** | A `Skill` created in Python with `@skill.script` decorators for in-process callable functions and `@skill.resource` for dynamic content |
|
||||
| **Class skill** | A self-contained skill class extending `ClassSkill`, bundling instructions, resources, and scripts |
|
||||
| **File skill** | A skill discovered from a `SKILL.md` file on disk, with reference documents and executable script files |
|
||||
| **`script_runner`** | A callable (sync or async) satisfying the `SkillScriptRunner` protocol — required when file skills have scripts |
|
||||
| **`SkillsProvider`** | Registers code-defined, class-based, and file-based skills in a single provider |
|
||||
|
||||
## Skills in This Sample
|
||||
|
||||
### volume-converter (code skill)
|
||||
|
||||
Defined entirely in Python code using decorators:
|
||||
|
||||
- **`@skill.resource`** — `conversion-table`: gallons↔liters conversion factors
|
||||
- **`@skill.script`** — `convert`: converts a value using a multiplication factor
|
||||
|
||||
Code scripts run **in-process** — no subprocess or external runner needed.
|
||||
|
||||
### temperature-converter (class skill)
|
||||
|
||||
Defined as a `TemperatureConverterSkill` class extending `ClassSkill`:
|
||||
|
||||
- **`@ClassSkill.resource`** — `temperature-conversion-formulas`: °F↔°C↔K formulas
|
||||
- **`@ClassSkill.script`** — `convert-temperature`: converts between temperature scales
|
||||
|
||||
Class-based scripts run **in-process** — no subprocess or external runner needed.
|
||||
|
||||
### unit-converter (file skill)
|
||||
|
||||
Discovered from `skills/unit-converter/SKILL.md`:
|
||||
|
||||
- **Reference**: `references/CONVERSION_TABLES.md` — supported unit conversions and their factors
|
||||
- **Script**: `scripts/convert.py` — converts a value using a multiplication factor (e.g. miles to kilometers)
|
||||
|
||||
File scripts are executed as **local Python subprocesses** via the
|
||||
`script_runner` callback.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SkillsProvider( │
|
||||
│ DeduplicatingSkillsSource( │
|
||||
│ AggregatingSkillsSource([ │
|
||||
│ FileSkillsSource("./skills", # file skills │
|
||||
│ script_runner=runner), │
|
||||
│ InMemorySkillsSource([ │
|
||||
│ volume_skill, # code skill │
|
||||
│ temp_converter, # class skill │
|
||||
│ ]), │
|
||||
│ ]) │
|
||||
│ ) │
|
||||
│ ) │
|
||||
└─────────────┬───────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ script_runner(skill, script, args) │
|
||||
│ │
|
||||
│ • Code scripts (@skill.script) → in-process call │
|
||||
│ • Class scripts (@ClassSkill.script) → in-process call │
|
||||
│ • File scripts (scripts/*.py) → subprocess via │
|
||||
│ the callback function │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Set environment variables (or create a `.env` file):
|
||||
|
||||
```
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://your-project.openai.azure.com/
|
||||
AZURE_OPENAI_MODEL=gpt-4o-mini
|
||||
```
|
||||
|
||||
Authenticate with Azure CLI:
|
||||
|
||||
```bash
|
||||
az login
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run samples/02-agents/skills/mixed_skills/mixed_skills.py
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
mixed_skills/
|
||||
├── mixed_skills.py # Main sample — wires code + file skills together
|
||||
├── README.md
|
||||
└── skills/
|
||||
└── unit-converter/ # File-based skill (discovered from SKILL.md)
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
│ └── CONVERSION_TABLES.md
|
||||
└── scripts/
|
||||
└── convert.py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [File-Based Skills Sample](../file_based_skill/)
|
||||
- [Code-Defined Skills Sample](../code_defined_skill/)
|
||||
- [Script Approval Sample](../script_approval/)
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
@@ -0,0 +1,248 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AggregatingSkillsSource,
|
||||
ClassSkill,
|
||||
DeduplicatingSkillsSource,
|
||||
FileSkillsSource,
|
||||
InlineSkill,
|
||||
InMemorySkillsSource,
|
||||
SkillFrontmatter,
|
||||
SkillsProvider,
|
||||
ToolApprovalMiddleware,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Add the skills folder root to sys.path so the shared subprocess_script_runner can be imported
|
||||
_SKILLS_ROOT = str(Path(__file__).resolve().parent.parent)
|
||||
if _SKILLS_ROOT not in sys.path:
|
||||
sys.path.insert(0, _SKILLS_ROOT)
|
||||
|
||||
from subprocess_script_runner import subprocess_script_runner # pyrefly: ignore[missing-import] # noqa: E402
|
||||
|
||||
"""
|
||||
Mixed Skills — Code, class, and file skills in a single agent
|
||||
|
||||
This sample demonstrates how to combine **code-defined skills** (with
|
||||
``@skill.script`` and ``@skill.resource`` decorators), **class-based skills**
|
||||
(subclassing ``ClassSkill``), and **file-based skills** (discovered from
|
||||
``SKILL.md`` files on disk) in a single agent using ``SkillsProvider`` and
|
||||
a ``SkillScriptRunner`` callable.
|
||||
|
||||
Key concepts shown:
|
||||
- Code skills with ``@skill.script``: executable Python functions the agent
|
||||
can invoke directly in-process.
|
||||
- Code skills with ``@skill.resource``: dynamic content the agent can read
|
||||
on demand.
|
||||
- Class skills: self-contained skill classes extending ``ClassSkill``.
|
||||
- File skills from disk: ``SKILL.md`` files with reference documents and
|
||||
executable script files.
|
||||
- ``script_runner``: routes **file-based** script execution
|
||||
through a callback, enabling custom handling (e.g. subprocess calls).
|
||||
Code-defined and class-based scripts run in-process automatically.
|
||||
|
||||
The sample registers three skills:
|
||||
1. **volume-converter** (code skill) — converts between gallons and liters using
|
||||
``@skill.script`` for conversion and ``@skill.resource`` for the factor table.
|
||||
2. **temperature-converter** (class skill) — converts between temperature scales
|
||||
(°F↔°C↔K) using a ``ClassSkill`` subclass.
|
||||
3. **unit-converter** (file skill) — converts between common units (miles↔km,
|
||||
pounds↔kg) via a subprocess-executed Python script discovered from
|
||||
``skills/unit-converter/SKILL.md``.
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Define a code skill with @skill.script and @skill.resource decorators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
volume_converter_skill = InlineSkill(
|
||||
frontmatter=SkillFrontmatter(
|
||||
name="volume-converter", description="Convert between gallons and liters using a conversion factor"
|
||||
),
|
||||
instructions=dedent("""\
|
||||
Use this skill when the user asks to convert between gallons and liters.
|
||||
|
||||
1. Review the conversion-table resource to find the correct factor.
|
||||
2. Use the convert script, passing the value and factor.
|
||||
"""),
|
||||
)
|
||||
|
||||
|
||||
@volume_converter_skill.resource(name="conversion-table", description="Volume conversion factors")
|
||||
def volume_table() -> Any:
|
||||
"""Return the volume conversion factor table."""
|
||||
return dedent("""\
|
||||
# Volume Conversion Table
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|---------|--------|---------|
|
||||
| gallons | liters | 3.78541 |
|
||||
| liters | gallons| 0.264172|
|
||||
""")
|
||||
|
||||
|
||||
@volume_converter_skill.script(name="convert", description="Convert a value: result = value × factor")
|
||||
def convert_volume(value: float, factor: float) -> str:
|
||||
"""Convert a value using a multiplication factor.
|
||||
|
||||
Args:
|
||||
value: The numeric value to convert.
|
||||
factor: Conversion factor from the table.
|
||||
|
||||
Returns:
|
||||
JSON string with the conversion result.
|
||||
"""
|
||||
result = round(value * factor, 4)
|
||||
return json.dumps({"value": value, "factor": factor, "result": result})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Define a class-based skill for temperature conversion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TemperatureConverterSkill(ClassSkill):
|
||||
"""A temperature-converter skill defined as a Python class.
|
||||
|
||||
Converts between temperature scales (Fahrenheit, Celsius, Kelvin).
|
||||
Resources and scripts are discovered automatically via decorators.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
frontmatter=SkillFrontmatter(
|
||||
name="temperature-converter",
|
||||
description="Convert between temperature scales (Fahrenheit, Celsius, Kelvin).",
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def instructions(self) -> str:
|
||||
return dedent("""\
|
||||
Use this skill when the user asks to convert temperatures.
|
||||
|
||||
1. Read the temperature-conversion-formulas resource to find the factor and offset
|
||||
for the requested conversion.
|
||||
2. Use the convert-temperature script, passing value, factor, and offset.
|
||||
3. Present the result clearly with both temperature scales.
|
||||
""")
|
||||
|
||||
@ClassSkill.resource(name="temperature-conversion-formulas")
|
||||
def formulas(self) -> str:
|
||||
"""Temperature conversion formulas reference table."""
|
||||
return dedent("""\
|
||||
# Temperature Conversion Formulas
|
||||
|
||||
Formula: **result = value × factor + offset**
|
||||
|
||||
| From | To | Factor | Offset |
|
||||
|-------------|-------------|----------|-----------|
|
||||
| Fahrenheit | Celsius | 0.555556 | -17.7778 |
|
||||
| Celsius | Fahrenheit | 1.8 | 32 |
|
||||
| Celsius | Kelvin | 1 | 273.15 |
|
||||
| Kelvin | Celsius | 1 | -273.15 |
|
||||
""")
|
||||
|
||||
@ClassSkill.script(name="convert-temperature")
|
||||
def convert_temperature(self, value: float, factor: float, offset: float = 0) -> str:
|
||||
"""Convert a temperature value using factor and offset from the formulas resource.
|
||||
|
||||
Args:
|
||||
value: The numeric temperature value to convert.
|
||||
factor: Conversion factor from the formulas resource.
|
||||
offset: Offset to add after multiplying (default 0).
|
||||
|
||||
Returns:
|
||||
JSON string with the conversion result.
|
||||
"""
|
||||
result = round(value * factor + offset, 4)
|
||||
return json.dumps({"value": value, "factor": factor, "offset": offset, "result": result})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Wire everything together and run the agent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the combined skills demo."""
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini")
|
||||
|
||||
# Create the chat client
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=endpoint,
|
||||
model=deployment,
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Create the SkillsProvider with code, class, and file skills.
|
||||
# The script_runner handles file-based scripts; code-defined and
|
||||
# class-based scripts run in-process automatically.
|
||||
temperature_converter = TemperatureConverterSkill()
|
||||
|
||||
skills_provider = SkillsProvider(
|
||||
DeduplicatingSkillsSource(
|
||||
AggregatingSkillsSource([
|
||||
FileSkillsSource(
|
||||
str(Path(__file__).parent / "skills"),
|
||||
script_runner=subprocess_script_runner,
|
||||
),
|
||||
InMemorySkillsSource([volume_converter_skill, temperature_converter]),
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
# Run the agent. All skill tools require approval by default; auto-approve
|
||||
# them so the sample runs unattended. See the script_approval /
|
||||
# skills_auto_approval samples for approval handling.
|
||||
async with Agent(
|
||||
client=client,
|
||||
instructions="You are a helpful assistant that can convert units, volumes, and temperatures.",
|
||||
context_providers=[skills_provider],
|
||||
middleware=[ToolApprovalMiddleware(auto_approval_rules=[SkillsProvider.all_tools_auto_approval_rule])],
|
||||
) as agent:
|
||||
# Ask the agent to use all three skills
|
||||
print("Converting with mixed skills (file + code + class)")
|
||||
print("-" * 60)
|
||||
session = agent.create_session()
|
||||
response = await agent.run(
|
||||
"I need three conversions: "
|
||||
"1) How many kilometers is a marathon (26.2 miles)? "
|
||||
"2) How many liters is a 5-gallon bucket? "
|
||||
"3) What is 98.6°F in Celsius?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Agent: {response}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Sample output:
|
||||
|
||||
Converting with mixed skills (file + code + class)
|
||||
------------------------------------------------------------
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. **26.2 miles → 42.16 km** (a marathon distance)
|
||||
2. **5 gallons → 18.93 liters**
|
||||
3. **98.6°F → 37.0°C**
|
||||
"""
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: unit-converter
|
||||
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
|
||||
license: MIT
|
||||
compatibility: Works with any model that supports tool use.
|
||||
allowed-tools: convert
|
||||
metadata:
|
||||
author: agent-framework-samples
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
When the user requests a unit conversion:
|
||||
1. First, review `references/CONVERSION_TABLES.md` to find the correct factor
|
||||
2. Run the `scripts/convert.py` script with `--value <number> --factor <factor>` (e.g. `--value 26.2 --factor 1.60934`)
|
||||
3. Present the converted value clearly with both units
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# Conversion Tables
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
@@ -0,0 +1,28 @@
|
||||
# Unit conversion script
|
||||
# Converts a value using a multiplication factor: result = value × factor
|
||||
#
|
||||
# Usage:
|
||||
# python scripts/convert.py --value 26.2 --factor 1.60934
|
||||
# python scripts/convert.py --value 75 --factor 2.20462
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert a value using a multiplication factor.",
|
||||
epilog="Examples:\n"
|
||||
" python scripts/convert.py --value 26.2 --factor 1.60934\n"
|
||||
" python scripts/convert.py --value 75 --factor 2.20462",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.")
|
||||
parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.")
|
||||
args = parser.parse_args()
|
||||
result = round(args.value * args.factor, 4)
|
||||
print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
# Skill Tool Approval — Human-in-the-Loop for Skill Tools
|
||||
|
||||
This sample demonstrates the **manual human-in-the-loop** approval pattern for
|
||||
skill tools. Every tool exposed by `SkillsProvider` (`load_skill`,
|
||||
`read_skill_resource`, and `run_skill_script`) requires host approval by
|
||||
default, so the agent pauses and returns approval requests that your
|
||||
application approves or rejects.
|
||||
|
||||
## How It Works
|
||||
|
||||
By default, skill tools require approval. The agent pauses before running any of
|
||||
them and returns approval requests instead:
|
||||
|
||||
1. The agent tries to call a skill tool (e.g. `load_skill` or `run_skill_script`) — execution is paused
|
||||
2. `result.user_input_requests` contains approval request(s) with function name and arguments
|
||||
3. The application inspects each request and decides to approve or reject
|
||||
4. `request.to_function_approval_response(approved=True|False)` creates the response
|
||||
5. The response is sent back via `agent.run(approval_response, session=session)`
|
||||
6. If approved, the tool runs; if rejected, the agent receives an error
|
||||
|
||||
## Key Components
|
||||
|
||||
- **Approval-by-default** — All skill tools require host approval; no extra configuration is needed
|
||||
- **`result.user_input_requests`** — Contains pending approval requests after `agent.run()`
|
||||
- **`request.to_function_approval_response()`** — Creates an approval or rejection response
|
||||
|
||||
To approve skill tools automatically instead of prompting for each one, use
|
||||
`ToolApprovalMiddleware` with one of the static auto-approval rules — see the
|
||||
[Skills Auto-Approval Sample](../skills_auto_approval/).
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model (e.g. `gpt-4o-mini`)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set the required environment variables in a `.env` file (see `python/.env.example`):
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_MODEL`: The name of your model deployment (defaults to `gpt-4o-mini`)
|
||||
|
||||
### Authentication
|
||||
|
||||
This sample uses `AzureCliCredential` for authentication. Run `az login` in your terminal before running the sample.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run samples/02-agents/skills/script_approval/script_approval.py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Skills Auto-Approval Sample](../skills_auto_approval/)
|
||||
- [File-Based Skills Sample](../file_based_skill/)
|
||||
- [Code-Defined Skills Sample](../code_defined_skill/)
|
||||
- [Mixed Skills Sample](../mixed_skills/)
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
@@ -0,0 +1,141 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from textwrap import dedent
|
||||
|
||||
from agent_framework import Agent, Content, InlineSkill, Message, SkillFrontmatter, SkillsProvider
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""
|
||||
Skill Tool Approval — Require human approval before running skill tools
|
||||
|
||||
Every tool exposed by :class:`SkillsProvider` (``load_skill``,
|
||||
``read_skill_resource``, and ``run_skill_script``) requires host approval by
|
||||
default. This sample shows the manual human-in-the-loop pattern: the agent
|
||||
pauses and returns approval requests, and the application approves or rejects
|
||||
each one before the agent continues.
|
||||
|
||||
How it works:
|
||||
1. A code-defined skill with a script is registered via SkillsProvider.
|
||||
2. Because skill tools require approval by default, the agent pauses and returns
|
||||
approval requests in ``result.user_input_requests`` instead of executing
|
||||
tools immediately.
|
||||
3. The application inspects each request and calls
|
||||
``request.to_function_approval_response(approved=True|False)`` to approve
|
||||
or reject.
|
||||
4. The approval response is sent back via ``agent.run(approval_response, session=session)``
|
||||
and the agent continues — running the tool if approved, or receiving an
|
||||
error if rejected.
|
||||
|
||||
To approve skill tools automatically instead of prompting, use
|
||||
``ToolApprovalMiddleware`` with one of the static auto-approval rules — see
|
||||
``samples/02-agents/skills/skills_auto_approval/skills_auto_approval.py``.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL (defaults to "gpt-4o-mini").
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Define a code skill with a script that performs a sensitive operation
|
||||
deployment_skill = InlineSkill(
|
||||
frontmatter=SkillFrontmatter(
|
||||
name="deployment", description="Tools for deploying application versions to production"
|
||||
),
|
||||
instructions=dedent("""\
|
||||
Use this skill when the user asks to deploy an application.
|
||||
|
||||
1. Run the deploy script with the version and environment parameters.
|
||||
"""),
|
||||
)
|
||||
|
||||
|
||||
@deployment_skill.script
|
||||
def deploy(version: str, environment: str = "staging") -> str:
|
||||
"""Deploy the application to the specified environment."""
|
||||
return f"Deployed version {version} to {environment}"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the skill script approval demo."""
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini")
|
||||
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=endpoint,
|
||||
model=deployment,
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Create the skills provider. All skill tools require approval by default.
|
||||
skills_provider = SkillsProvider(
|
||||
source=[deployment_skill],
|
||||
)
|
||||
|
||||
async with Agent(
|
||||
client=client,
|
||||
instructions="You are a deployment assistant. Use the deployment skill to deploy applications.",
|
||||
context_providers=[skills_provider],
|
||||
) as agent:
|
||||
session = agent.create_session()
|
||||
|
||||
print("Starting agent with skill tool approval (the default)...")
|
||||
print("-" * 60)
|
||||
|
||||
# Step 1: Send the user request — the agent will try to call the script
|
||||
query = "Deploy the latest application version 2.5.0 to the production environment"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query, session=session)
|
||||
|
||||
# Step 2: Handle approval requests (with sessions, context is
|
||||
# maintained automatically). Collect a response for every request and
|
||||
# send them in one run so the loop always makes progress.
|
||||
while result.user_input_requests:
|
||||
approval_responses: list[Content] = []
|
||||
for request in result.user_input_requests:
|
||||
if request.function_call is None:
|
||||
# Not a function-approval request; reject it so the run can proceed.
|
||||
approval_responses.append(request.to_function_approval_response(approved=False))
|
||||
continue
|
||||
print("\nApproval needed:")
|
||||
print(f" Function: {request.function_call.name}")
|
||||
print(f" Arguments: {request.function_call.arguments}")
|
||||
|
||||
# In a real application, prompt the user here
|
||||
approved = True # Change to False to see rejection
|
||||
print(f" Decision: {'Approved' if approved else 'Rejected'}")
|
||||
approval_responses.append(request.to_function_approval_response(approved=approved))
|
||||
|
||||
# Send the approval responses — session preserves conversation history
|
||||
result = await agent.run(Message(role="user", contents=approval_responses), session=session)
|
||||
|
||||
print(f"\nAgent: {result}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Sample output:
|
||||
|
||||
Starting agent with skill tool approval (the default)...
|
||||
------------------------------------------------------------
|
||||
User: Deploy the latest application version 2.5.0 to the production environment
|
||||
|
||||
Approval needed:
|
||||
Function: load_skill
|
||||
Arguments: {"skill_name": "deployment"}
|
||||
Decision: Approved
|
||||
|
||||
Approval needed:
|
||||
Function: run_skill_script
|
||||
Arguments: {"skill_name": "deployment", "script_name": "deploy", ...}
|
||||
Decision: Approved
|
||||
|
||||
Agent: Successfully deployed version 2.5.0 to production.
|
||||
"""
|
||||
@@ -0,0 +1,94 @@
|
||||
# Skill Filtering — FilteringSkillsSource
|
||||
|
||||
This sample demonstrates how to use `FilteringSkillsSource` to control
|
||||
which file-based skills an agent sees by applying a predicate.
|
||||
|
||||
## Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **`FileSkillsSource`** | Discovers skills from `SKILL.md` files on disk |
|
||||
| **`FilteringSkillsSource`** | Wraps a source and applies a predicate to include or exclude skills |
|
||||
| **`DeduplicatingSkillsSource`** | Removes duplicate skill names (first-one-wins) |
|
||||
|
||||
## Skills in This Sample
|
||||
|
||||
### volume-converter (kept)
|
||||
|
||||
Converts between gallons and liters via `scripts/convert.py`.
|
||||
|
||||
### length-converter (filtered out)
|
||||
|
||||
Converts between miles ↔ km, feet ↔ meters. Excluded by the filter predicate
|
||||
so the agent never sees it.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ FileSkillsSource("./skills") │
|
||||
│ discovers: volume-converter, length-converter │
|
||||
└─────────────┬────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ FilteringSkillsSource(predicate=...) │
|
||||
│ keeps: volume-converter │
|
||||
│ drops: length-converter │
|
||||
└─────────────┬────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ DeduplicatingSkillsSource → SkillsProvider │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
> **Note:** `FilteringSkillsSource` works with any source — file-based,
|
||||
> in-memory, custom, or a mix. If you only need a single skill, point
|
||||
> `FileSkillsSource` directly at that skill's directory instead of filtering.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Set environment variables (or create a `.env` file):
|
||||
|
||||
```
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://your-project.openai.azure.com/
|
||||
FOUNDRY_MODEL=gpt-4o-mini
|
||||
```
|
||||
|
||||
Authenticate with Azure CLI:
|
||||
|
||||
```bash
|
||||
az login
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run samples/02-agents/skills/skill_filtering/skill_filtering.py
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
skill_filtering/
|
||||
├── skill_filtering.py
|
||||
├── README.md
|
||||
└── skills/
|
||||
├── volume-converter/
|
||||
│ ├── SKILL.md
|
||||
│ └── scripts/
|
||||
│ └── convert.py
|
||||
└── length-converter/
|
||||
├── SKILL.md
|
||||
└── scripts/
|
||||
└── convert.py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [File-Based Skills Sample](../file_based_skill/)
|
||||
- [Mixed Skills Sample](../mixed_skills/)
|
||||
- [Code-Defined Skills Sample](../code_defined_skill/)
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
@@ -0,0 +1,113 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
DeduplicatingSkillsSource,
|
||||
FileSkillsSource,
|
||||
FilteringSkillsSource,
|
||||
SkillsProvider,
|
||||
ToolApprovalMiddleware,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Add the skills folder root to sys.path so the shared subprocess_script_runner can be imported
|
||||
_SKILLS_ROOT = str(Path(__file__).resolve().parent.parent)
|
||||
if _SKILLS_ROOT not in sys.path:
|
||||
sys.path.insert(0, _SKILLS_ROOT)
|
||||
|
||||
from subprocess_script_runner import subprocess_script_runner # pyrefly: ignore[missing-import] # noqa: E402
|
||||
|
||||
"""
|
||||
Skill Filtering — Using FilteringSkillsSource with file-based skills
|
||||
|
||||
This sample demonstrates how to use **FilteringSkillsSource** to control
|
||||
which skills an agent sees. Although this example uses file-based skills,
|
||||
``FilteringSkillsSource`` works equally well with in-memory skills,
|
||||
custom sources, or any combination of them.
|
||||
|
||||
A single ``skills/`` directory contains two file-based skills discovered via
|
||||
``FileSkillsSource``:
|
||||
|
||||
- **volume-converter** — converts between gallons and liters
|
||||
- **length-converter** — converts between miles ↔ km, feet ↔ meters
|
||||
|
||||
A ``FilteringSkillsSource`` wraps the file source and excludes the
|
||||
``length-converter`` skill, so the agent only sees the volume-converter skill.
|
||||
|
||||
Note: if you only need a single skill, you could point ``FileSkillsSource``
|
||||
directly at that skill's directory and skip filtering entirely. This sample
|
||||
intentionally points at the parent directory to demonstrate filtering.
|
||||
|
||||
Key concepts shown:
|
||||
|
||||
1. **FileSkillsSource** — discovers skills from ``SKILL.md`` files on disk.
|
||||
2. **FilteringSkillsSource** — applies a predicate to include or exclude
|
||||
specific skills by name (or any custom logic).
|
||||
3. **DeduplicatingSkillsSource** — ensures no duplicate skill names survive.
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the skill filtering demo."""
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini")
|
||||
|
||||
# 1. Create the chat client
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=endpoint,
|
||||
model=deployment,
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# 2. Compose the source pipeline:
|
||||
# file discovery → filter out length-converter → deduplicate
|
||||
skills_dir = Path(__file__).parent / "skills"
|
||||
source = DeduplicatingSkillsSource(
|
||||
FilteringSkillsSource(
|
||||
FileSkillsSource(str(skills_dir), script_runner=subprocess_script_runner),
|
||||
# Only keep the volume-converter skill
|
||||
predicate=lambda skill, context: skill.frontmatter.name != "length-converter",
|
||||
)
|
||||
)
|
||||
|
||||
skills_provider = SkillsProvider(source)
|
||||
|
||||
# 3. Run the agent — it can only see the volume-converter skill. All skill
|
||||
# tools require approval by default; auto-approve them so the sample runs
|
||||
# unattended. See the script_approval / skills_auto_approval samples for
|
||||
# approval handling.
|
||||
async with Agent(
|
||||
client=client,
|
||||
instructions="You are a helpful assistant that can convert units.",
|
||||
context_providers=[skills_provider],
|
||||
middleware=[ToolApprovalMiddleware(auto_approval_rules=[SkillsProvider.all_tools_auto_approval_rule])],
|
||||
) as agent:
|
||||
print("Skill filtering demo")
|
||||
print("-" * 60)
|
||||
session = agent.create_session()
|
||||
response = await agent.run("How many liters is a 5-gallon bucket?", session=session)
|
||||
print(f"Agent: {response}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Sample output:
|
||||
|
||||
Skill filtering demo
|
||||
------------------------------------------------------------
|
||||
Agent: A 5-gallon bucket is equal to **18.9271 liters**.
|
||||
|
||||
I used the conversion factor: 5 × 3.78541 = 18.9271
|
||||
"""
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: length-converter
|
||||
description: Convert between common length units (miles, km, feet, meters) using a multiplication factor.
|
||||
license: MIT
|
||||
compatibility: Works with any model that supports tool use.
|
||||
allowed-tools: convert
|
||||
metadata:
|
||||
author: agent-framework-samples
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
When the user requests a length conversion, run the `scripts/convert.py`
|
||||
script with `--value <number> --factor <factor>`.
|
||||
|
||||
Common factors:
|
||||
- miles → km: 1.60934
|
||||
- km → miles: 0.621371
|
||||
- feet → meters: 0.3048
|
||||
- meters → feet: 3.28084
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
"""Convert a value by multiplying it with a factor."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--value", type=float, required=True)
|
||||
parser.add_argument("--factor", type=float, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
result = round(args.value * args.factor, 4)
|
||||
print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: volume-converter
|
||||
description: Convert between gallons and liters using a conversion factor.
|
||||
license: MIT
|
||||
compatibility: Works with any model that supports tool use.
|
||||
allowed-tools: convert
|
||||
metadata:
|
||||
author: agent-framework-samples
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
When the user requests a volume conversion:
|
||||
1. Run the `scripts/convert.py` script with `--value <number> --factor <factor>`
|
||||
2. Use factor 3.78541 for gallons → liters, or 0.264172 for liters → gallons
|
||||
3. Present the converted value clearly with both units
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
"""Convert a value by multiplying it with a factor."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--value", type=float, required=True)
|
||||
parser.add_argument("--factor", type=float, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
result = round(args.value * args.factor, 4)
|
||||
print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
# Skills Auto-Approval — Configure Auto-Approval Rules for Skill Tools
|
||||
|
||||
This sample demonstrates how to configure **auto-approval rules** for skill
|
||||
tools using `ToolApprovalMiddleware`. Every tool exposed by `SkillsProvider`
|
||||
(`load_skill`, `read_skill_resource`, and `run_skill_script`) requires host
|
||||
approval by default. Auto-approval rules let you selectively bypass the approval
|
||||
prompt for safe operations.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. A code-defined unit-converter skill (with a resource and a script) is registered via `SkillsProvider`.
|
||||
2. The agent installs `ToolApprovalMiddleware` with `SkillsProvider.read_only_tools_auto_approval_rule`.
|
||||
3. The read-only tools (`load_skill`, `read_skill_resource`) are approved automatically.
|
||||
4. `run_skill_script` still requires explicit approval and is handled with the standard `result.user_input_requests` loop.
|
||||
|
||||
## Auto-Approval Rules
|
||||
|
||||
`SkillsProvider` exposes two static rules to pass to `ToolApprovalMiddleware(auto_approval_rules=[...])`:
|
||||
|
||||
- **`SkillsProvider.read_only_tools_auto_approval_rule`** — approves only the read-only tools (`load_skill`, `read_skill_resource`), while still prompting for `run_skill_script`.
|
||||
- **`SkillsProvider.all_tools_auto_approval_rule`** — approves every skill tool, including `run_skill_script` (no manual approval loop needed).
|
||||
|
||||
Both rules reject any call carrying a `server_label`, so they stay scoped to this provider's local tools and never auto-approve a same-named hosted tool.
|
||||
|
||||
> **Note:** To use auto-approval rules, the agent must have `ToolApprovalMiddleware` in its middleware stack.
|
||||
|
||||
## Key Components
|
||||
|
||||
- **`ToolApprovalMiddleware(auto_approval_rules=[...])`** — Drives the approval handshake and applies the rules
|
||||
- **`SkillsProvider.read_only_tools_auto_approval_rule`** — Auto-approves read-only skill tools
|
||||
- **`SkillsProvider.all_tools_auto_approval_rule`** — Auto-approves all skill tools
|
||||
- **`SkillsProvider.LOAD_SKILL_TOOL_NAME` / `READ_SKILL_RESOURCE_TOOL_NAME` / `RUN_SKILL_SCRIPT_TOOL_NAME`** — Tool-name constants for building custom rules
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model (e.g. `gpt-4o-mini`)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set the required environment variables in a `.env` file (see `python/.env.example`):
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_MODEL`: The name of your model deployment (defaults to `gpt-4o-mini`)
|
||||
|
||||
### Authentication
|
||||
|
||||
This sample uses `AzureCliCredential` for authentication. Run `az login` in your terminal before running the sample.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run samples/02-agents/skills/skills_auto_approval/skills_auto_approval.py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Skill Tool Approval Sample](../script_approval/) — manual human-in-the-loop approval
|
||||
- [Code-Defined Skills Sample](../code_defined_skill/)
|
||||
- [File-Based Skills Sample](../file_based_skill/)
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
@@ -0,0 +1,191 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
Content,
|
||||
InlineSkill,
|
||||
InlineSkillResource,
|
||||
Message,
|
||||
SkillFrontmatter,
|
||||
SkillsProvider,
|
||||
ToolApprovalMiddleware,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""
|
||||
Skills Auto-Approval — Configure auto-approval rules for skill tools
|
||||
|
||||
Every tool exposed by :class:`SkillsProvider` (``load_skill``,
|
||||
``read_skill_resource``, and ``run_skill_script``) requires host approval by
|
||||
default. Rather than prompting for every call, this sample uses
|
||||
:class:`ToolApprovalMiddleware` with a static auto-approval rule so that the
|
||||
read-only tools are approved automatically while script execution still
|
||||
requires explicit user approval.
|
||||
|
||||
How it works:
|
||||
1. A code-defined unit-converter skill (with a resource and a script) is
|
||||
registered via SkillsProvider.
|
||||
2. The agent installs ``ToolApprovalMiddleware`` with
|
||||
``SkillsProvider.read_only_tools_auto_approval_rule``. This auto-approves
|
||||
``load_skill`` and ``read_skill_resource`` while still prompting for
|
||||
``run_skill_script``.
|
||||
3. The application handles the remaining ``run_skill_script`` approval requests
|
||||
via the standard ``result.user_input_requests`` loop.
|
||||
|
||||
Available auto-approval rules:
|
||||
- ``SkillsProvider.read_only_tools_auto_approval_rule`` — approves only the
|
||||
read-only tools (``load_skill``, ``read_skill_resource``).
|
||||
- ``SkillsProvider.all_tools_auto_approval_rule`` — approves every skill tool,
|
||||
including ``run_skill_script`` (no manual approval loop needed).
|
||||
|
||||
To use auto-approval rules, the agent must have ``ToolApprovalMiddleware`` in
|
||||
its middleware stack.
|
||||
|
||||
Prerequisites:
|
||||
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
|
||||
- FOUNDRY_MODEL (defaults to "gpt-4o-mini").
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# A code-defined unit-converter skill with a resource (read-only) and a script.
|
||||
unit_converter_skill = InlineSkill(
|
||||
frontmatter=SkillFrontmatter(
|
||||
name="unit-converter", description="Convert between common units using a conversion factor"
|
||||
),
|
||||
instructions=dedent("""\
|
||||
Use this skill when the user asks to convert between units.
|
||||
|
||||
1. Review the conversion-tables resource to find the factor for the
|
||||
requested conversion.
|
||||
2. Use the convert script, passing the value and factor from the table.
|
||||
"""),
|
||||
resources=[
|
||||
InlineSkillResource(
|
||||
name="conversion-tables",
|
||||
content=dedent("""\
|
||||
# Conversion Tables
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
"""),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@unit_converter_skill.script(name="convert", description="Convert a value: result = value × factor")
|
||||
def convert_units(value: float, factor: float, **kwargs: Any) -> str:
|
||||
"""Convert a value using a multiplication factor: result = value × factor.
|
||||
|
||||
Args:
|
||||
value: The numeric value to convert.
|
||||
factor: Conversion factor from the conversion table.
|
||||
**kwargs: Runtime keyword arguments from ``agent.run()``.
|
||||
|
||||
Returns:
|
||||
JSON string with the inputs and converted result.
|
||||
"""
|
||||
result = round(value * factor, 2)
|
||||
return json.dumps({"value": value, "factor": factor, "result": result})
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the skills auto-approval demo."""
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini")
|
||||
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=endpoint,
|
||||
model=deployment,
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
skills_provider = SkillsProvider(unit_converter_skill)
|
||||
|
||||
# Install ToolApprovalMiddleware with the read-only auto-approval rule.
|
||||
# load_skill and read_skill_resource are approved automatically; the agent
|
||||
# only pauses for run_skill_script.
|
||||
#
|
||||
# To approve every skill tool without prompting, swap the rule for
|
||||
# SkillsProvider.all_tools_auto_approval_rule (the manual approval loop
|
||||
# below then becomes a no-op).
|
||||
approval_middleware = ToolApprovalMiddleware(
|
||||
auto_approval_rules=[SkillsProvider.read_only_tools_auto_approval_rule]
|
||||
)
|
||||
|
||||
async with Agent(
|
||||
client=client,
|
||||
instructions="You are a helpful assistant that can convert units.",
|
||||
context_providers=[skills_provider],
|
||||
middleware=[approval_middleware],
|
||||
) as agent:
|
||||
session = agent.create_session()
|
||||
|
||||
print("Converting units with skill tools and read-only auto-approval")
|
||||
print("-" * 60)
|
||||
|
||||
query = "How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query, session=session)
|
||||
|
||||
# Read-only tools (load_skill, read_skill_resource) were auto-approved.
|
||||
# Only run_skill_script reaches this loop and needs explicit approval.
|
||||
# Collect a response for every request and send them in one run so the
|
||||
# loop always makes progress.
|
||||
while result.user_input_requests:
|
||||
approval_responses: list[Content] = []
|
||||
for request in result.user_input_requests:
|
||||
if request.function_call is None:
|
||||
# Not a function-approval request; reject it so the run can proceed.
|
||||
approval_responses.append(request.to_function_approval_response(approved=False))
|
||||
continue
|
||||
print("\nApproval needed:")
|
||||
print(f" Function: {request.function_call.name}")
|
||||
print(f" Arguments: {request.function_call.arguments}")
|
||||
|
||||
# In a real application, prompt the user here.
|
||||
approved = True
|
||||
print(f" Decision: {'Approved' if approved else 'Rejected'}")
|
||||
approval_responses.append(request.to_function_approval_response(approved=approved))
|
||||
|
||||
result = await agent.run(Message(role="user", contents=approval_responses), session=session)
|
||||
|
||||
print(f"\nAgent: {result}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Sample output:
|
||||
|
||||
Converting units with skill tools and read-only auto-approval
|
||||
------------------------------------------------------------
|
||||
User: How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?
|
||||
|
||||
Approval needed:
|
||||
Function: run_skill_script
|
||||
Arguments: {"skill_name": "unit-converter", "script_name": "convert", ...}
|
||||
Decision: Approved
|
||||
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. 26.2 miles -> 42.16 km (a marathon distance)
|
||||
2. 75 kg -> 165.35 lbs
|
||||
"""
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Sample subprocess-based skill script runner.
|
||||
Executes file-based skill scripts as local Python subprocesses.
|
||||
This is provided for demonstration purposes only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import FileSkill, FileSkillScript
|
||||
|
||||
|
||||
def subprocess_script_runner(
|
||||
skill: FileSkill, script: FileSkillScript, args: dict[str, Any] | list[str] | None = None
|
||||
) -> str:
|
||||
"""Run a skill script as a local Python subprocess.
|
||||
Uses ``FileSkillScript.full_path`` as the script path, converts the
|
||||
``args`` to CLI arguments, and returns captured output.
|
||||
Args:
|
||||
skill: The file-based skill that owns the script.
|
||||
script: The file-based script to run.
|
||||
args: Optional arguments. A ``list[str]`` is forwarded as
|
||||
positional CLI arguments. Passing a ``dict`` or any other
|
||||
type raises :class:`TypeError` — file-based scripts expect
|
||||
positional arguments as a JSON array of strings.
|
||||
Returns:
|
||||
The combined stdout/stderr output, or an error message.
|
||||
Raises:
|
||||
TypeError: If ``args`` is not a ``list[str]`` or ``None``, or if
|
||||
any list element is not a string.
|
||||
"""
|
||||
script_path = Path(script.full_path)
|
||||
if not script_path.is_file():
|
||||
return f"Error: Script file not found: {script_path}"
|
||||
cmd = [sys.executable, str(script_path)]
|
||||
if isinstance(args, list):
|
||||
for item in args:
|
||||
if not isinstance(item, str):
|
||||
raise TypeError(
|
||||
f"File-based skill scripts only accept string CLI arguments "
|
||||
f"but received a {type(item).__name__}. "
|
||||
f"All array elements must be strings."
|
||||
)
|
||||
cmd.extend(args)
|
||||
elif args is not None:
|
||||
raise TypeError(
|
||||
f"Expected a list of CLI arguments but received {type(args).__name__}. "
|
||||
f"File-based skill scripts expect positional arguments as a list of strings."
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
cwd=str(script_path.parent),
|
||||
)
|
||||
output = result.stdout
|
||||
if result.stderr:
|
||||
output += f"\nStderr:\n{result.stderr}"
|
||||
if result.returncode != 0:
|
||||
output += f"\nScript exited with code {result.returncode}"
|
||||
return output.strip() or "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"Error: Script '{script.name}' timed out after 30 seconds."
|
||||
except OSError as e:
|
||||
return f"Error: Failed to execute script '{script.name}': {e}"
|
||||
Reference in New Issue
Block a user