Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
<Folder Name="/Samples/02-agents/AgentSkills/">
<File Path="samples/02-agents/AgentSkills/README.md" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_BasicSkills/Agent_Step01_BasicSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_ScriptExecutionWithCodeInterpreter/Agent_Step02_ScriptExecutionWithCodeInterpreter.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
var skillsProvider = new FileAgentSkillsProvider(skillPath: Path.Combine(AppContext.BaseDirectory, "skills"));

// --- Agent Setup ---
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.AsAIAgent(new ChatClientAgentOptions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>

<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>

<!-- Copy skills directory to output -->
<ItemGroup>
<None Include="skills\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.

// This sample demonstrates how to use Agent Skills with script execution via the hosted code interpreter.
// When FileAgentSkillScriptExecutor.HostedCodeInterpreter() is configured, the agent can load and execute scripts
// from skill resources using the LLM provider's built-in code interpreter.
//
// This sample includes the password-generator skill:
// - A Python script for generating secure passwords

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;

// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";

// --- Skills Provider with Script Execution ---
// Discovers skills and enables script execution via the hosted code interpreter
var skillsProvider = new FileAgentSkillsProvider(
skillPath: Path.Combine(AppContext.BaseDirectory, "skills"),
options: new FileAgentSkillsProviderOptions
{
ScriptExecutor = FileAgentSkillScriptExecutor.HostedCodeInterpreter()
});

// --- Agent Setup ---
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.AsAIAgent(new ChatClientAgentOptions
{
Name = "SkillsAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant that can generate secure passwords.",
},
AIContextProviders = [skillsProvider],
});

// --- Example: Password generation with script execution ---
Console.WriteLine("Example: Generating a password with a skill script");
Console.WriteLine("---------------------------------------------------");
AgentResponse response = await agent.RunAsync("Generate a secure password for my database account.");
Console.WriteLine($"Agent: {response.Text}\n");
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Script Execution with Code Interpreter

This sample demonstrates how to use **Agent Skills** with **script execution** via the hosted code interpreter.

## What's Different from Step01?

In the [basic skills sample](../Agent_Step01_BasicSkills/), skills only provide instructions and resources as text. This sample adds **script execution** — the agent can load Python scripts from skill resources and execute them using the LLM provider's built-in code interpreter.

This is enabled by configuring `FileAgentSkillScriptExecutor.HostedCodeInterpreter()` on the skills provider options:

```csharp
var skillsProvider = new FileAgentSkillsProvider(
skillPath: Path.Combine(AppContext.BaseDirectory, "skills"),
options: new FileAgentSkillsProviderOptions
{
ScriptExecutor = FileAgentSkillScriptExecutor.HostedCodeInterpreter()
});
```

## Skills Included

### password-generator
Generates secure passwords using a Python script with configurable length and complexity.
- `scripts/generate.py` — Password generation script
- `references/PASSWORD_GUIDELINES.md` — Recommended length and symbol sets by use case

## Project Structure

```
Agent_Step02_ScriptExecutionWithCodeInterpreter/
├── Program.cs
├── Agent_Step02_ScriptExecutionWithCodeInterpreter.csproj
└── skills/
└── password-generator/
├── SKILL.md
├── scripts/
│ └── generate.py
└── references/
└── PASSWORD_GUIDELINES.md
```

## Running the Sample

### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model that supports code interpreter

### Setup
1. Set environment variables:
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```

2. Run the sample:
```bash
dotnet run
```

### Example

The sample asks the agent to generate a secure password. The agent:
1. Loads the password-generator skill
2. Reads the `generate.py` script via `read_skill_resource`
3. Executes the script using the code interpreter with appropriate parameters
4. Returns the generated password

## Learn More

- [Agent Skills Specification](https://agentskills.io/)
- [Step01: Basic Skills](../Agent_Step01_BasicSkills/) — Skills without script execution
- [Microsoft Agent Framework Documentation](../../../../../docs/)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: password-generator
description: Generate secure passwords using a Python script. Use when asked to create passwords or credentials.
---

# Password Generator

This skill generates secure passwords using a Python script.

## Usage

When the user requests a password:
1. First, review `references/PASSWORD_GUIDELINES.md` to determine the recommended password length and character sets for the user's use case
2. Load `scripts/generate.py` and adjust its parameters (length, character set) based on the guidelines and user's requirements
3. Execute the script
4. Present the generated password clearly
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Password Generation Guidelines

## General Rules

- Never reuse passwords across services.
- Always use cryptographically secure randomness (e.g., `random.SystemRandom()`).
- Avoid dictionary words, keyboard patterns, and personal information.

## Recommended Settings by Use Case

| Use Case | Min Length | Character Set | Example |
|-----------------------|-----------|----------------------------------------|--------------------------|
| Web account | 16 | Upper + lower + digits + symbols | `G7!kQp@2xM#nW9$z` |
| Database credential | 24 | Upper + lower + digits + symbols | `aR3$vK8!mN2@pQ7&xL5#wY` |
| Wi-Fi / network key | 20 | Upper + lower + digits + symbols | `Ht4&jL9!rP2#mK7@xQ` |
| API key / token | 32 | Upper + lower + digits (no symbols) | `k8Rm3xQ7nW2pL9vT4jH6yA` |
| Encryption passphrase | 32 | Upper + lower + digits + symbols | `Xp4!kR8@mN2#vQ7&jL9$wT` |

## Symbol Sets

- **Standard symbols**: `!@#$%^&*()-_=+`
- **Extended symbols**: `~`{}[]|;:'",.<>?/\`
- **Safe symbols** (URL/shell-safe): `!@#$&*-_=+`
- If the target system restricts symbols, use only the **safe** set.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Password generator script
# Usage: Adjust 'length' as needed, then run

import random
import string

length = 16 # desired length

pool = string.ascii_lowercase + string.ascii_uppercase + string.digits + string.punctuation
password = "".join(random.SystemRandom().choice(pool) for _ in range(length))
print(f"Generated password ({length} chars): {password}")
1 change: 1 addition & 0 deletions dotnet/samples/02-agents/AgentSkills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Samples demonstrating Agent Skills capabilities.
| Sample | Description |
|--------|-------------|
| [Agent_Step01_BasicSkills](Agent_Step01_BasicSkills/) | Using Agent Skills with a ChatClientAgent, including progressive disclosure and skill resources |
| [Agent_Step02_ScriptExecutionWithCodeInterpreter](Agent_Step02_ScriptExecutionWithCodeInterpreter/) | Using Agent Skills with script execution via the hosted code interpreter |
21 changes: 12 additions & 9 deletions dotnet/src/Microsoft.Agents.AI/Skills/FileAgentSkill.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;
Expand All @@ -13,7 +15,8 @@ namespace Microsoft.Agents.AI;
/// and a markdown body with instructions. Resource files referenced in the body are validated at
/// discovery time and read from disk on demand.
/// </remarks>
internal sealed class FileAgentSkill
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class FileAgentSkill
{
/// <summary>
/// Initializes a new instance of the <see cref="FileAgentSkill"/> class.
Expand All @@ -22,8 +25,8 @@ internal sealed class FileAgentSkill
/// <param name="body">The SKILL.md content after the closing <c>---</c> delimiter.</param>
/// <param name="sourcePath">Absolute path to the directory containing this skill.</param>
/// <param name="resourceNames">Relative paths of resource files referenced in the skill body.</param>
public FileAgentSkill(
SkillFrontmatter frontmatter,
internal FileAgentSkill(
FileAgentSkillFrontmatter frontmatter,
string body,
string sourcePath,
IReadOnlyList<string>? resourceNames = null)
Expand All @@ -37,20 +40,20 @@ public FileAgentSkill(
/// <summary>
/// Gets the parsed YAML frontmatter (name and description).
/// </summary>
public SkillFrontmatter Frontmatter { get; }
public FileAgentSkillFrontmatter Frontmatter { get; }

/// <summary>
/// Gets the SKILL.md body content (without the YAML frontmatter).
/// Gets the directory path where the skill was discovered.
/// </summary>
public string Body { get; }
public string SourcePath { get; }

/// <summary>
/// Gets the directory path where the skill was discovered.
/// Gets the SKILL.md body content (without the YAML frontmatter).
/// </summary>
public string SourcePath { get; }
internal string Body { get; }

/// <summary>
/// Gets the relative paths of resource files referenced in the skill body (e.g., "references/FAQ.md").
/// </summary>
public IReadOnlyList<string> ResourceNames { get; }
internal IReadOnlyList<string> ResourceNames { get; }
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;

/// <summary>
/// Parsed YAML frontmatter from a SKILL.md file, containing the skill's name and description.
/// </summary>
internal sealed class SkillFrontmatter
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class FileAgentSkillFrontmatter
{
/// <summary>
/// Initializes a new instance of the <see cref="SkillFrontmatter"/> class.
/// Initializes a new instance of the <see cref="FileAgentSkillFrontmatter"/> class.
/// </summary>
/// <param name="name">Skill name.</param>
/// <param name="description">Skill description.</param>
public SkillFrontmatter(string name, string description)
internal FileAgentSkillFrontmatter(string name, string description)
{
this.Name = Throw.IfNullOrWhitespace(name);
this.Description = Throw.IfNullOrWhitespace(description);
Expand Down
22 changes: 14 additions & 8 deletions dotnet/src/Microsoft.Agents.AI/Skills/FileAgentSkillLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Agents.AI;

Expand All @@ -20,7 +22,8 @@ namespace Microsoft.Agents.AI;
/// Each file is validated for YAML frontmatter and resource integrity. Invalid skills are excluded
/// with logged warnings. Resource paths are checked against path traversal and symlink escape attacks.
/// </remarks>
internal sealed partial class FileAgentSkillLoader
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed partial class FileAgentSkillLoader
{
private const string SkillFileName = "SKILL.md";
private const int MaxSearchDepth = 2;
Expand All @@ -33,13 +36,16 @@ internal sealed partial class FileAgentSkillLoader
// Example: "---\nname: foo\n---\nBody" → Group 1: "name: foo\n"
private static readonly Regex s_frontmatterRegex = new(@"\A\uFEFF?^---\s*$(.+?)^---\s*$", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(5));

// Matches markdown links to local resource files. Group 1 = relative file path.
// Matches resource file references in skill markdown. Group 1 = relative file path.
// Supports two forms:
// 1. Markdown links: [text](path/file.ext)
// 2. Backtick-quoted paths: `path/file.ext`
// Supports optional ./ or ../ prefixes; excludes URLs (no ":" in the path character class).
// Intentionally conservative: only matches paths with word characters, hyphens, dots,
// and forward slashes. Paths with spaces or special characters are not supported.
// Examples: [doc](refs/FAQ.md) → "refs/FAQ.md", [s](./s.json) → "./s.json",
// Examples: [doc](refs/FAQ.md) → "refs/FAQ.md", `./scripts/run.py` → "./scripts/run.py",
// [p](../shared/doc.txt) → "../shared/doc.txt"
private static readonly Regex s_resourceLinkRegex = new(@"\[.*?\]\((\.?\.?/?[\w][\w\-./]*\.\w+)\)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
private static readonly Regex s_resourceLinkRegex = new(@"(?:\[.*?\]\(|`)(\.?\.?/?[\w][\w\-./]*\.\w+)(?:\)|`)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));

// Matches YAML "key: value" lines. Group 1 = key, Group 2 = quoted value, Group 3 = unquoted value.
// Accepts single or double quotes; the lazy quantifier trims trailing whitespace on unquoted values.
Expand Down Expand Up @@ -111,7 +117,7 @@ internal Dictionary<string, FileAgentSkill> DiscoverAndLoadSkills(IEnumerable<st
/// <exception cref="InvalidOperationException">
/// The resource is not registered, resolves outside the skill directory, or does not exist.
/// </exception>
internal async Task<string> ReadSkillResourceAsync(FileAgentSkill skill, string resourceName, CancellationToken cancellationToken = default)
public async Task<string> ReadSkillResourceAsync(FileAgentSkill skill, string resourceName, CancellationToken cancellationToken = default)
{
resourceName = NormalizeResourcePath(resourceName);

Expand Down Expand Up @@ -189,7 +195,7 @@ private static void SearchDirectoriesForSkills(string directory, List<string> re

string content = File.ReadAllText(skillFilePath, Encoding.UTF8);

if (!this.TryParseSkillDocument(content, skillFilePath, out SkillFrontmatter frontmatter, out string body))
if (!this.TryParseSkillDocument(content, skillFilePath, out FileAgentSkillFrontmatter frontmatter, out string body))
{
return null;
}
Expand All @@ -208,7 +214,7 @@ private static void SearchDirectoriesForSkills(string directory, List<string> re
resourceNames: resourceNames);
}

private bool TryParseSkillDocument(string content, string skillFilePath, out SkillFrontmatter frontmatter, out string body)
private bool TryParseSkillDocument(string content, string skillFilePath, out FileAgentSkillFrontmatter frontmatter, out string body)
{
frontmatter = null!;
body = null!;
Expand Down Expand Up @@ -264,7 +270,7 @@ private bool TryParseSkillDocument(string content, string skillFilePath, out Ski
return false;
}

frontmatter = new SkillFrontmatter(name, description);
frontmatter = new FileAgentSkillFrontmatter(name, description);
body = content.Substring(match.Index + match.Length).TrimStart();

return true;
Expand Down
Loading
Loading