Python: Support skill scripts execution (#4558)

* support skill scripts execution

* fix mixed line endings

* address comments and fix syntax issues

* use few try/except instead of one

* change samples

* validate either script path or script resource is set not both

* fix: separate LLM args from runtime kwargs in skill script execution

* address pr review comments

* address PR review comments

* Update python/packages/core/agent_framework/_skills.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/core/agent_framework/_skills.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/core/agent_framework/_skills.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* 1. Fixing the caching bug where parameters_schema would re-inspect on every call when the result was None
   2. Updating the arguments tool description to be more generic (not CLI-specific)

* fix failing tests

* address pr review comments

* address pr review comments

* allow resource function returning any instead of sting

* address PR review comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
SergeyMenshykh
2026-03-11 18:28:30 +00:00
committed by GitHub
Unverified
parent 2f8fd5f82f
commit 23ebfbc937
27 changed files with 2994 additions and 528 deletions
+55
View File
@@ -0,0 +1,55 @@
# 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. |
| [**mixed_skills**](mixed_skills/) | Combine code-defined and file-based skills in a single agent. Uses a code-defined volume-converter and a file-based unit-converter. |
| [**script_approval**](script_approval/) | Require human-in-the-loop approval before executing skill scripts |
## 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 Skills
| Aspect | File-Based | Code-Defined |
|--------|-----------|--------------|
| Definition | `SKILL.md` files on disk | `Skill` instances in Python |
| Resources | Static files in `references/` and `assets/` directories | Callable functions via `@skill.resource` decorator |
| Scripts | Python files in `scripts/` directory (executed via subprocess) | Callable functions via `@skill.script` decorator (executed in-process) |
| Discovery | Automatic via `skill_paths` parameter | Explicit via `skills` parameter |
| Dynamic content | No (static files only) | Yes (functions can generate content at runtime) |
Both 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`)
@@ -1,68 +0,0 @@
# Agent Skills Sample
This sample demonstrates how to use **Agent Skills** with a `SkillsProvider` in the Microsoft Agent Framework.
## 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 the progressive disclosure pattern:
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
## Skills Included
### expense-report
Policy-based expense filing with spending limits, receipt requirements, and approval workflows.
- `references/POLICY_FAQ.md` — Detailed expense policy Q&A
- `assets/expense-report-template.md` — Submission template
## Project Structure
```
basic_skill/
├── basic_skill.py
├── README.md
└── skills/
└── expense-report/
├── SKILL.md
├── references/
│ └── POLICY_FAQ.md
└── assets/
└── expense-report-template.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`):
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: 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/basic_skill/basic_skill.py
```
### Examples
The sample runs two examples:
1. **Expense policy FAQ** — Asks about tip reimbursement; the agent loads the expense-report skill and reads the FAQ resource
2. **Filing an expense report** — Multi-turn conversation to draft an expense report using the template asset
## Learn More
- [Agent Skills Specification](https://agentskills.io/)
- [Microsoft Agent Framework Documentation](../../../../../docs/)
@@ -1,88 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from pathlib import Path
from agent_framework import Agent, SkillsProvider
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""
Agent Skills Sample
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 the progressive disclosure pattern:
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
This sample includes the expense-report skill:
- Policy-based expense filing with references and assets
"""
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
"""Run the Agent Skills demo."""
# --- Configuration ---
endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"]
deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini")
# --- 1. Create the chat client ---
client = AzureOpenAIResponsesClient(
project_endpoint=endpoint,
deployment_name=deployment,
credential=AzureCliCredential(),
)
# --- 2. Create the skills provider ---
# Discovers skills from the 'skills' directory and makes them available to the agent
skills_dir = Path(__file__).parent / "skills"
skills_provider = SkillsProvider(skill_paths=str(skills_dir))
# --- 3. Create the agent with skills ---
async with Agent(
client=client,
instructions="You are a helpful assistant.",
context_providers=[skills_provider],
) as agent:
# --- Example 1: Expense policy question (loads FAQ resource) ---
print("Example 1: Checking expense policy FAQ")
print("---------------------------------------")
response1 = await agent.run(
"Are tips reimbursable? I left a 25% tip on a taxi ride and want to know if that's covered."
)
print(f"Agent: {response1}\n")
# --- Example 2: Filing an expense report (uses template asset) ---
print("Example 2: Filing an expense report")
print("---------------------------------------")
session = agent.create_session()
response2 = await agent.run(
"I had 3 client dinners and a $1,200 flight last week. "
"Return a draft expense report and ask about any missing details.",
session=session,
)
print(f"Agent: {response2}\n")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Example 1: Checking expense policy FAQ
---------------------------------------
Agent: Tips up to 20% are reimbursable for meals, taxi/ride-share, and hotel housekeeping.
Since you left a 25% tip, the portion above 20% would require written justification...
Example 2: Filing an expense report
---------------------------------------
Agent: Here's a draft expense report based on what you've told me. I'll need a few more details...
"""
@@ -1,40 +0,0 @@
---
name: expense-report
description: File and validate employee expense reports according to Contoso company policy. Use when asked about expense submissions, reimbursement rules, receipt requirements, spending limits, or expense categories.
metadata:
author: contoso-finance
version: "2.1"
---
# Expense Report
## Categories and Limits
| Category | Limit | Receipt | Approval |
|---|---|---|---|
| Meals — solo | $50/day | >$25 | No |
| Meals — team/client | $75/person | Always | Manager if >$200 total |
| Lodging | $250/night | Always | Manager if >3 nights |
| Ground transport | $100/day | >$15 | No |
| Airfare | Economy | Always | Manager; VP if >$1,500 |
| Conference/training | $2,000/event | Always | Manager + L&D |
| Office supplies | $100 | Yes | No |
| Software/subscriptions | $50/month | Yes | Manager if >$200/year |
## Filing Process
1. Collect receipts — must show vendor, date, amount, payment method.
2. Categorize per table above.
3. Use template: [assets/expense-report-template.md](assets/expense-report-template.md).
4. For client/team meals: list attendee names and business purpose.
5. Submit — auto-approved if <$500; manager if $500$2,000; VP if >$2,000.
6. Reimbursement: 10 business days via direct deposit.
## Policy Rules
- Submit within 30 days of transaction.
- Alcohol is never reimbursable.
- Foreign currency: convert to USD at transaction-date rate; note original currency and amount.
- Mixed personal/business travel: only business portion reimbursable; provide comparison quotes.
- Lost receipts (>$25): file Lost Receipt Affidavit from Finance. Max 2 per quarter.
- For policy questions not covered above, consult the FAQ: [references/POLICY_FAQ.md](references/POLICY_FAQ.md). Answers should be based on what this document and the FAQ state.
@@ -1,5 +0,0 @@
# Expense Report Template
| Date | Category | Vendor | Description | Amount (USD) | Original Currency | Original Amount | Attendees | Business Purpose | Receipt Attached |
|------|----------|--------|-------------|--------------|-------------------|-----------------|-----------|------------------|------------------|
| | | | | | | | | | Yes or No |
@@ -1,55 +0,0 @@
# Expense Policy — Frequently Asked Questions
## Meals
**Q: Can I expense coffee or snacks during the workday?**
A: Daily coffee/snacks under $10 are not reimbursable (considered personal). Coffee purchased during a client meeting or team working session is reimbursable as a team meal.
**Q: What if a team dinner exceeds the per-person limit?**
A: The $75/person limit applies as a guideline. Overages up to 20% are accepted with a written justification (e.g., "client dinner at venue chosen by client"). Overages beyond 20% require pre-approval from your VP.
**Q: Do I need to list every attendee?**
A: Yes. For client meals, list the client's name and company. For team meals, list all employee names. For groups over 10, you may attach a separate attendee list.
## Travel
**Q: Can I book a premium economy or business class flight?**
A: Economy class is the standard. Premium economy is allowed for flights over 6 hours. Business class requires VP pre-approval and is generally reserved for flights over 10 hours or medical accommodation.
**Q: What about ride-sharing (Uber/Lyft) vs. rental cars?**
A: Use ride-sharing for trips under 30 miles round-trip. Rent a car for multi-day travel or when ride-sharing would exceed $100/day. Always choose the compact/standard category unless traveling with 3+ people.
**Q: Are tips reimbursable?**
A: Tips up to 20% are reimbursable for meals, taxi/ride-share, and hotel housekeeping. Tips above 20% require justification.
## Lodging
**Q: What if the $250/night limit isn't enough for the city I'm visiting?**
A: For high-cost cities (New York, San Francisco, London, Tokyo, Sydney), the limit is automatically increased to $350/night. No additional approval is needed. For other locations where rates are unusually high (e.g., during a major conference), request a per-trip exception from your manager before booking.
**Q: Can I stay with friends/family instead and get a per-diem?**
A: No. Contoso reimburses actual lodging costs only, not per-diems.
## Subscriptions and Software
**Q: Can I expense a personal productivity tool?**
A: Software must be directly related to your job function. Tools like IDE licenses, design software, or project management apps are reimbursable. General productivity apps (note-taking, personal calendar) are not, unless your manager confirms a business need in writing.
**Q: What about annual subscriptions?**
A: Annual subscriptions over $200 require manager approval before purchase. Submit the approval email with your expense report.
## Receipts and Documentation
**Q: My receipt is faded/damaged. What do I do?**
A: Try to obtain a duplicate from the vendor. If not possible, submit a Lost Receipt Affidavit (available from the Finance SharePoint site). You're limited to 2 affidavits per quarter.
**Q: Do I need a receipt for parking meters or tolls?**
A: For amounts under $15, no receipt is required — just note the date, location, and amount. For $15 and above, a receipt or bank/credit card statement excerpt is required.
## Approval and Reimbursement
**Q: My manager is on leave. Who approves my report?**
A: Expense reports can be approved by your skip-level manager or any manager designated as an alternate approver in the expense system.
**Q: Can I submit expenses from a previous quarter?**
A: The standard 30-day window applies. Expenses older than 30 days require a written explanation and VP approval. Expenses older than 90 days are not reimbursable except in extraordinary circumstances (extended leave, medical emergency) with CFO approval.
@@ -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`):
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: 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,173 @@
# 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, Skill, SkillResource, SkillsProvider
from agent_framework.azure import AzureOpenAIResponsesClient
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 = Skill(
name="unit-converter",
description="Convert between common units using a conversion factor",
content=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=[
SkillResource(
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(..., 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["AZURE_AI_PROJECT_ENDPOINT"]
deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini")
client = AzureOpenAIResponsesClient(
project_endpoint=endpoint,
deployment_name=deployment,
credential=AzureCliCredential(),
)
# Create the skills provider with the code-defined skill
skills_provider = SkillsProvider(
skills=[unit_converter_skill],
)
async with Agent(
client=client,
instructions="You are a helpful assistant that can convert units.",
context_providers=[skills_provider],
) as agent:
print("Converting units")
print("-" * 60)
response = await agent.run(
"How many kilometers is a marathon (26.2 miles)? "
"And how many pounds is 75 kilograms?",
precision=2,
)
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.
"""
@@ -1,57 +0,0 @@
# Code-Defined Agent Skills Sample
This sample demonstrates how to create **Agent Skills** in Python code, without needing `SKILL.md` files on disk.
## What are Code-Defined Skills?
While file-based skills use `SKILL.md` files discovered on disk, code-defined skills let you define skills entirely in Python using `Skill` and `SkillResource` classes. Three patterns are shown:
1. **Basic Code Skill** — Create a `Skill` directly with static resources (inline content)
2. **Dynamic Resources** — Attach callable resources via the `@skill.resource` decorator that generate content at invocation time
3. **Dynamic Resources with kwargs** — Attach a callable resource that accepts `**kwargs` to receive runtime arguments passed via `agent.run()`, useful for injecting request-scoped context (user tokens, session data)
All patterns can be combined with file-based skills in a single `SkillsProvider`.
## Project Structure
```
code_skill/
├── code_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`):
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: 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_skill/code_skill.py
```
### Examples
The sample runs two examples:
1. **Code style question** — Uses Pattern 1 (static resources): the agent loads the `code-style` skill and reads the `style-guide` resource to answer naming convention questions
2. **Project info question** — Uses Patterns 2 & 3 (dynamic resources with kwargs): the agent reads the dynamically generated `team-roster` resource and the `environment` resource which receives `app_version` via runtime kwargs
## Learn More
- [Agent Skills Specification](https://agentskills.io/)
- [File-based Skills Sample](../basic_skill/)
- [Microsoft Agent Framework Documentation](../../../../../docs/)
@@ -1,161 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import sys
from textwrap import dedent
from typing import Any
from agent_framework import Agent, Skill, SkillResource, SkillsProvider
from agent_framework.azure import AzureOpenAIResponsesClient
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 patterns are shown:
Pattern 1: Basic Code Skill
Create a Skill instance directly with static resources (inline content).
Pattern 2: Dynamic Resources
Create a Skill and attach callable resources via the @skill.resource
decorator. Resources can be sync or async functions that generate content at
invocation time.
Pattern 3: Dynamic Resources with kwargs
Attach a callable resource that accepts **kwargs to receive runtime
arguments passed via agent.run(). This is useful for injecting
request-scoped context (user tokens, session data) into skill resources.
Both patterns can be combined with file-based skills in a single SkillsProvider.
"""
# Load environment variables from .env file
load_dotenv()
# Pattern 1: Basic Code Skill — direct construction with static resources
code_style_skill = Skill(
name="code-style",
description="Coding style guidelines and conventions for the team",
content=dedent("""\
Use this skill when answering questions about coding style, conventions,
or best practices for the team.
"""),
resources=[
SkillResource(
name="style-guide",
content=dedent("""\
# Team Coding Style Guide
## General Rules
- Use 4-space indentation (no tabs)
- Maximum line length: 120 characters
- Use type annotations on all public functions
- Use Google-style docstrings
## Naming Conventions
- Classes: PascalCase (e.g., UserAccount)
- Functions/methods: snake_case (e.g., get_user_name)
- Constants: UPPER_SNAKE_CASE (e.g., MAX_RETRIES)
- Private members: prefix with underscore (e.g., _internal_state)
"""),
),
],
)
# Pattern 2: Dynamic Resources — @skill.resource decorator
project_info_skill = Skill(
name="project-info",
description="Project status and configuration information",
content=dedent("""\
Use this skill for questions about the current project status,
environment configuration, or team structure.
"""),
)
@project_info_skill.resource
def environment(**kwargs: Any) -> str:
"""Get current environment configuration."""
# Access runtime kwargs passed via agent.run(app_version="...")
app_version = kwargs.get("app_version", "unknown")
env = os.environ.get("APP_ENV", "development")
region = os.environ.get("APP_REGION", "us-east-1")
return f"""\
# Environment Configuration
- App Version: {app_version}
- Environment: {env}
- Region: {region}
- Python: {sys.version}
"""
@project_info_skill.resource(name="team-roster", description="Current team members and roles")
def get_team_roster() -> str:
"""Return the team roster."""
return """\
# Team Roster
| Name | Role |
|--------------|-------------------|
| Alice Chen | Tech Lead |
| Bob Smith | Backend Engineer |
| Carol Davis | Frontend Engineer |
"""
async def main() -> None:
"""Run the code-defined skills demo."""
endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"]
deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini")
client = AzureOpenAIResponsesClient(
project_endpoint=endpoint,
deployment_name=deployment,
credential=AzureCliCredential(),
)
# Create the skills provider with both code-defined skills
skills_provider = SkillsProvider(
skills=[code_style_skill, project_info_skill],
)
async with Agent(
client=client,
instructions="You are a helpful assistant for our development team.",
context_providers=[skills_provider],
) as agent:
# Example 1: Code style question (Pattern 1 — static resources)
print("Example 1: Code style question")
print("-------------------------------")
response = await agent.run("What naming convention should I use for class attributes?")
print(f"Agent: {response}\n")
# Example 2: Project info question (Pattern 2 & 3 — dynamic resources with kwargs)
print("Example 2: Project info question")
print("---------------------------------")
# Pass app_version as a runtime kwarg; it flows to the environment() resource via **kwargs
response = await agent.run("What environment are we running in and who is on the team?", app_version="2.4.1")
print(f"Agent: {response}\n")
"""
Expected output:
Example 1: Code style question
-------------------------------
Agent: Based on our team's coding style guide, class attributes should follow
snake_case naming. Private attributes use an underscore prefix (_internal_state).
Constants use UPPER_SNAKE_CASE (MAX_RETRIES).
Example 2: Project info question
---------------------------------
Agent: We're running app version 2.4.1 in the development environment
in us-east-1. The team consists of Alice Chen (Tech Lead), Bob Smith
(Backend Engineer), and Carol Davis (Frontend Engineer).
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -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`):
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: 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,94 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import sys
from pathlib import Path
from agent_framework import Agent, SkillsProvider
from agent_framework.azure import AzureOpenAIResponsesClient
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 # 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["AZURE_AI_PROJECT_ENDPOINT"]
deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini")
# Create the chat client
client = AzureOpenAIResponsesClient(
project_endpoint=endpoint,
deployment_name=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(
skill_paths=str(skills_dir),
script_runner=subprocess_script_runner,
)
# Create the agent with skills
async with Agent(
client=client,
instructions="You are a helpful assistant.",
context_providers=[skills_provider],
) 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)
response = await agent.run(
"How many kilometers is a marathon (26.2 miles)? "
"And how many pounds is 75 kilograms?"
)
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,11 @@
---
name: unit-converter
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
---
## 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
@@ -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,29 @@
# 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,100 @@
# Mixed Skills — Code Skills and File Skills
This sample demonstrates how to combine **code-defined skills** and
**file-based skills** in a single agent using a `SkillScriptRunner` callable
and `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 |
| **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 both code-defined 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.
### 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( │
│ skill_paths="./skills", # file skills │
│ skills=[volume_converter_skill], # code skills │
│ script_runner=runner, │
│ ) │
└─────────────┬───────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ script_runner(skill, script, args) │
│ │
│ • Code scripts (@skill.script) → in-process call │
│ • File scripts (scripts/*.py) → subprocess via │
│ the callback function │
└─────────────────────────────────────────────────────────────┘
```
## Prerequisites
Set environment variables (or create a `.env` file):
```
AZURE_AI_PROJECT_ENDPOINT=https://your-project.openai.azure.com/
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=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,160 @@
# 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,
Skill,
SkillsProvider,
)
from agent_framework.azure import AzureOpenAIResponsesClient
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 # noqa: E402
"""
Mixed Skills — Code skills and file skills in a single agent
This sample demonstrates how to combine **code-defined skills** (with
``@skill.script`` and ``@skill.resource`` decorators) 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.
- 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 scripts (``@skill.script``) run in-process automatically.
The sample registers two skills:
1. **volume-converter** (code skill) — converts between gallons and liters using
``@skill.script`` for conversion and ``@skill.resource`` for the factor table.
2. **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 = Skill(
name="volume-converter",
description="Convert between gallons and liters using a conversion factor",
content=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. Wire everything together and run the agent
# ---------------------------------------------------------------------------
async def main() -> None:
"""Run the combined skills demo."""
endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"]
deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini")
# Create the chat client
client = AzureOpenAIResponsesClient(
project_endpoint=endpoint,
deployment_name=deployment,
credential=AzureCliCredential(),
)
# Create the SkillsProvider with both code and file skills.
# The script_runner handles file-based scripts; code-defined scripts
# (@skill.script) run in-process automatically.
skills_dir = Path(__file__).parent / "skills"
skills_provider = SkillsProvider(
skill_paths=str(skills_dir),
skills=[volume_converter_skill],
script_runner=subprocess_script_runner,
)
# Run the agent
async with Agent(
client=client,
instructions="You are a helpful assistant that can convert units.",
context_providers=[skills_provider],
) as agent:
# Ask the agent to use both skills
print("Converting units")
print("-" * 60)
response = await agent.run(
"How many kilometers is a marathon (26.2 miles)? "
"And how many liters is a 5-gallon bucket?"
)
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. **5 gallons → 18.93 liters**
I used the conversion factors from each skill's reference table.
"""
@@ -0,0 +1,11 @@
---
name: unit-converter
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
---
## 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
@@ -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,29 @@
# 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,50 @@
# Script Approval — Human-in-the-Loop for Skill Scripts
This sample demonstrates how to require **human approval** before executing skill scripts using the `require_script_approval=True` option on `SkillsProvider`.
## How It Works
When `require_script_approval=True` is set, the agent pauses before executing any skill script and returns approval requests instead:
1. The agent tries to call `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 script executes; if rejected, the agent receives an error
## Key Components
- **`require_script_approval=True`** — Gates all script execution on human approval
- **`result.user_input_requests`** — Contains pending approval requests after `agent.run()`
- **`request.to_function_approval_response()`** — Creates an approval or rejection response
## 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`):
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: 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
- [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,124 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from textwrap import dedent
from agent_framework import Agent, Skill, SkillsProvider
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""
Skill Script Approval — Require human approval before executing skill scripts
This sample demonstrates how to use ``require_script_approval=True`` on
:class:`SkillsProvider` so that every call to ``run_skill_script`` is
gated by a human-in-the-loop approval step.
How it works:
1. A code-defined skill with a script is registered via SkillsProvider.
2. ``require_script_approval=True`` causes the agent to pause and return
approval requests in ``result.user_input_requests`` instead of executing
scripts 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 — executing the script if approved, or receiving
an error if rejected.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME (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 = Skill(
name="deployment",
description="Tools for deploying application versions to production",
content=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["AZURE_AI_PROJECT_ENDPOINT"]
deployment = os.environ.get("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME", "gpt-4o-mini")
client = AzureOpenAIResponsesClient(
project_endpoint=endpoint,
deployment_name=deployment,
credential=AzureCliCredential(),
)
# Create the skills provider with script approval enabled
skills_provider = SkillsProvider(
skills=[deployment_skill],
require_script_approval=True,
)
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 script approval enabled...")
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 — just send the approval response)
while result.user_input_requests:
for request in result.user_input_requests:
print(f"\nApproval needed:")
print(f" Function: {request.function_call.name}") # type: ignore[union-attr]
print(f" Arguments: {request.function_call.arguments}") # type: ignore[union-attr]
# In a real application, prompt the user here
approved = True # Change to False to see rejection
print(f" Decision: {'Approved' if approved else 'Rejected'}")
# Send the approval response — session preserves conversation history
approval_response = request.to_function_approval_response(approved=approved)
result = await agent.run(approval_response, session=session)
print(f"\nAgent: {result}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Starting agent with skill script approval enabled...
------------------------------------------------------------
User: Deploy version 2.5.0 to production
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,75 @@
# 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 Skill, SkillScript
def subprocess_script_runner(skill: Skill, script: SkillScript, args: dict[str, Any] | None = None) -> str:
"""Run a skill script as a local Python subprocess.
Resolves the script's absolute path from the skill directory, converts
the ``args`` dict to CLI flags, and returns captured output.
Args:
skill: The skill that owns the script.
script: The script to run.
args: Optional arguments forwarded as CLI flags.
Returns:
The combined stdout/stderr output, or an error message.
"""
if not skill.path:
return f"Error: Skill '{skill.name}' has no directory path."
if not script.path:
return f"Error: Script '{script.name}' has no file path. Only file-based scripts can be executed locally."
script_path = Path(skill.path) / script.path
if not script_path.is_file():
return f"Error: Script file not found: {script_path}"
cmd = [sys.executable, str(script_path)]
# Convert args dict to CLI flags
if args:
for key, value in args.items():
if isinstance(value, bool):
if value:
cmd.append(f"--{key}")
elif value is not None:
cmd.append(f"--{key}")
cmd.append(str(value))
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}"