mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into peibekwe/workflow-unit-tests
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#
|
||||
# Runs the .NET sample verification tool, which builds and executes sample projects
|
||||
# and verifies their output using deterministic checks and AI-powered verification.
|
||||
#
|
||||
# Results are displayed as a GitHub Job Summary and the CSV report is uploaded as an artifact.
|
||||
#
|
||||
|
||||
name: dotnet-verify-samples
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
category:
|
||||
description: "Sample category to run (blank for all)"
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- ""
|
||||
- "01-get-started"
|
||||
- "02-agents"
|
||||
- "03-workflows"
|
||||
parallelism:
|
||||
description: "Max parallel sample runs"
|
||||
required: false
|
||||
default: "8"
|
||||
type: string
|
||||
schedule:
|
||||
- cron: "0 6 * * 1-5" # Weekdays at 6:00 UTC
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
verify-samples:
|
||||
runs-on: ubuntu-latest
|
||||
environment: 'integration'
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
workflow-samples
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.2.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Run verify-samples
|
||||
id: verify
|
||||
working-directory: dotnet
|
||||
shell: bash
|
||||
run: |
|
||||
CATEGORY_ARG=""
|
||||
if [ -n "$CATEGORY_INPUT" ]; then
|
||||
CATEGORY_ARG="--category $CATEGORY_INPUT"
|
||||
fi
|
||||
|
||||
dotnet run --project eng/verify-samples -- \
|
||||
$CATEGORY_ARG \
|
||||
--parallel "$PARALLELISM" \
|
||||
--md results.md \
|
||||
--csv results.csv \
|
||||
--log results.log
|
||||
env:
|
||||
CATEGORY_INPUT: ${{ github.event.inputs.category || '' }}
|
||||
PARALLELISM: ${{ github.event.inputs.parallelism || '8' }}
|
||||
# OpenAI Models
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_CHAT_MODEL_NAME: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_REASONING_MODEL_NAME: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
# Azure OpenAI Models
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
|
||||
# Azure AI Foundry
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
|
||||
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
|
||||
|
||||
- name: Write Job Summary
|
||||
if: always()
|
||||
working-directory: dotnet
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f results.md ]; then
|
||||
cat results.md >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "⚠️ No results.md generated — verify-samples may have failed to start." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Upload results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: verify-samples-results
|
||||
path: |
|
||||
dotnet/results.csv
|
||||
dotnet/results.log
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Fail if samples failed
|
||||
if: always() && steps.verify.outcome == 'failure'
|
||||
shell: bash
|
||||
run: exit 1
|
||||
@@ -230,3 +230,6 @@ local.settings.json
|
||||
# Database files
|
||||
*.db
|
||||
python/dotnet-ref
|
||||
|
||||
# Generated filtered solution files (created by eng/scripts/New-FilteredSolution.ps1)
|
||||
dotnet/filtered-*.slnx
|
||||
|
||||
@@ -28,7 +28,7 @@ Welcome to Microsoft's comprehensive multi-language framework for building, orch
|
||||
Python
|
||||
|
||||
```bash
|
||||
pip install agent-framework --pre
|
||||
pip install agent-framework
|
||||
# This will install all sub-packages, see `python/packages` for individual packages.
|
||||
# It may take a minute on first install on Windows.
|
||||
```
|
||||
@@ -90,7 +90,7 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
|
||||
Create a simple Azure Responses Agent that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```python
|
||||
# pip install agent-framework --pre
|
||||
# pip install agent-framework
|
||||
# Use `az login` to authenticate with Azure CLI
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
+2
-1
@@ -25,7 +25,7 @@ dotnet run --project eng/verify-samples -- Agent_Step02_StructuredOutput Agent_S
|
||||
dotnet run --project eng/verify-samples -- --parallel 8 --log results.log
|
||||
|
||||
# Combine options
|
||||
dotnet run --project eng/verify-samples -- --category 03-workflows --parallel 4 --log results.log --csv results.csv
|
||||
dotnet run --project eng/verify-samples -- --category 03-workflows --parallel 4 --log results.log --csv results.csv --md results.md
|
||||
```
|
||||
|
||||
### Required Environment Variables
|
||||
@@ -40,6 +40,7 @@ Individual samples require their own env vars (e.g., `AZURE_AI_PROJECT_ENDPOINT`
|
||||
|
||||
- `--log results.log` — detailed per-sample log with stdout/stderr, AI reasoning, and a summary
|
||||
- `--csv results.csv` — tabular summary with Sample, ProjectPath, Status, FailedChecks, and Failures columns
|
||||
- `--md results.md` — Markdown summary with results table and collapsible failure details (suitable for GitHub PR comments)
|
||||
|
||||
## Sample Categories
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, `AIFunct
|
||||
- **Async**: Use `Async` suffix for methods returning `Task`/`ValueTask`
|
||||
- **Private classes**: Should be `sealed` unless subclassed
|
||||
- **Config**: Read from environment variables with `UPPER_SNAKE_CASE` naming
|
||||
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking
|
||||
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking; test methods returning `Task`/`ValueTask` must use the `Async` suffix.
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="12.8.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.11.0" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.4.2" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
|
||||
@@ -106,6 +106,9 @@
|
||||
<File Path="samples/02-agents/AgentSkills/README.md" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Agent_Step02_CodeDefinedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Writes a Markdown summary of sample verification results.
|
||||
/// </summary>
|
||||
internal static class MarkdownResultWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the results to a Markdown file at the specified path.
|
||||
/// </summary>
|
||||
public static async Task WriteAsync(
|
||||
string path,
|
||||
IReadOnlyList<VerificationResult> orderedResults,
|
||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
||||
TimeSpan elapsed)
|
||||
{
|
||||
var passCount = orderedResults.Count(r => r.Passed);
|
||||
var failCount = orderedResults.Count(r => !r.Passed);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("# Sample Verification Results");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"**{passCount} passed, {failCount} failed, {skipped.Count} skipped** | Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Results table
|
||||
sb.AppendLine("| Sample | Status | Failed Checks | Failures |");
|
||||
sb.AppendLine("|--------|--------|---------------|----------|");
|
||||
|
||||
foreach (var result in orderedResults)
|
||||
{
|
||||
var status = result.Passed ? "✅ PASSED" : "❌ FAILED";
|
||||
var failedChecks = result.Failures.Count;
|
||||
var failures = MdEscape(string.Join("; ", result.Failures));
|
||||
sb.AppendLine($"| {MdEscape(result.SampleName)} | {status} | {failedChecks} | {failures} |");
|
||||
}
|
||||
|
||||
foreach (var (name, reason) in skipped)
|
||||
{
|
||||
sb.AppendLine($"| {MdEscape(name)} | ⏭️ SKIPPED | 0 | {MdEscape(reason)} |");
|
||||
}
|
||||
|
||||
// Collapsible AI reasoning details for failures
|
||||
var failures2 = orderedResults.Where(r => !r.Passed && !string.IsNullOrEmpty(r.AIReasoning)).ToList();
|
||||
if (failures2.Count > 0)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("## Failure Details");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var result in failures2)
|
||||
{
|
||||
sb.AppendLine($"<details><summary><strong>{HtmlEscape(result.SampleName)}</strong></summary>");
|
||||
sb.AppendLine();
|
||||
if (result.Failures.Count > 0)
|
||||
{
|
||||
foreach (var failure in result.Failures)
|
||||
{
|
||||
sb.AppendLine($"- {MdEscape(failure)}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("**AI Reasoning:**");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine(result.AIReasoning);
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("</details>");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(path, sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes pipe characters and newlines for use inside Markdown table cells.
|
||||
/// </summary>
|
||||
private static string MdEscape(string value)
|
||||
{
|
||||
return value.Replace("|", "\\|").Replace("\n", " ").Replace("\r", "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes HTML special characters for use inside HTML tags.
|
||||
/// </summary>
|
||||
private static string HtmlEscape(string value)
|
||||
{
|
||||
return value.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
// dotnet run -- --parallel 16 # Run up to 16 samples concurrently
|
||||
// dotnet run -- --log results.log # Write sequential log to file
|
||||
// dotnet run -- --csv results.csv # Write CSV summary to file
|
||||
// dotnet run -- --md results.md # Write Markdown summary to file
|
||||
//
|
||||
// Required environment variables (for AI-powered samples):
|
||||
// AZURE_OPENAI_ENDPOINT
|
||||
@@ -90,6 +91,13 @@ try
|
||||
Console.WriteLine($"CSV written to: {options.CsvFilePath}");
|
||||
}
|
||||
|
||||
// Write Markdown summary
|
||||
if (options.MarkdownFilePath is not null)
|
||||
{
|
||||
await MarkdownResultWriter.WriteAsync(options.MarkdownFilePath, orderedResults, run.Skipped, stopwatch.Elapsed);
|
||||
Console.WriteLine($"Markdown written to: {options.MarkdownFilePath}");
|
||||
}
|
||||
|
||||
return orderedResults.Any(r => !r.Passed) ? 1 : 0;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -17,6 +17,11 @@ internal sealed class VerifyOptions
|
||||
/// </summary>
|
||||
public string? CsvFilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to write a Markdown summary file, or <c>null</c> to skip.
|
||||
/// </summary>
|
||||
public string? MarkdownFilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to write a sequential log file, or <c>null</c> to skip.
|
||||
/// </summary>
|
||||
@@ -49,6 +54,7 @@ internal sealed class VerifyOptions
|
||||
var categoryFilter = ExtractArg(argList, "--category");
|
||||
var logFilePath = ExtractArg(argList, "--log");
|
||||
var csvFilePath = ExtractArg(argList, "--csv");
|
||||
var markdownFilePath = ExtractArg(argList, "--md");
|
||||
|
||||
int maxParallelism = 8;
|
||||
var parallelArg = ExtractArg(argList, "--parallel");
|
||||
@@ -98,6 +104,7 @@ internal sealed class VerifyOptions
|
||||
MaxParallelism = maxParallelism,
|
||||
LogFilePath = logFilePath,
|
||||
CsvFilePath = csvFilePath,
|
||||
MarkdownFilePath = markdownFilePath,
|
||||
Samples = samples,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ This sample demonstrates how to use **file-based Agent Skills** with a `ChatClie
|
||||
|
||||
- Discovering skills from `SKILL.md` files on disk via `AgentFileSkillsSource`
|
||||
- The progressive disclosure pattern: advertise → load → read resources → run scripts
|
||||
- Using the `AgentSkillsProvider` constructor with a skill directory path and script executor
|
||||
- Using the `AgentSkillsProvider` constructor with a skill directory path and script runner
|
||||
- Running file-based scripts (Python) via a subprocess-based executor
|
||||
|
||||
## Skills Included
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to define Agent Skills as C# classes using AgentClassSkill.
|
||||
// Class-based skills bundle all components into a single class implementation.
|
||||
|
||||
using System.Text.Json;
|
||||
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";
|
||||
|
||||
// --- Class-Based Skill ---
|
||||
// Instantiate the skill class.
|
||||
var unitConverter = new UnitConverterSkill();
|
||||
|
||||
// --- Skills Provider ---
|
||||
var skillsProvider = new AgentSkillsProvider(unitConverter);
|
||||
|
||||
// --- Agent Setup ---
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant that can convert units.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Example: Unit conversion ---
|
||||
Console.WriteLine("Converting units with class-based skills");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
|
||||
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
|
||||
/// <summary>
|
||||
/// A unit-converter skill defined as a C# class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Class-based skills bundle all components (name, description, body, resources, scripts)
|
||||
/// into a single class.
|
||||
/// </remarks>
|
||||
internal sealed class UnitConverterSkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new(
|
||||
"unit-converter",
|
||||
"Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.");
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string Instructions => """
|
||||
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.
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
CreateResource(
|
||||
"conversion-table",
|
||||
"""
|
||||
# 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 |
|
||||
"""),
|
||||
];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
CreateScript("convert", ConvertUnits),
|
||||
];
|
||||
|
||||
private static string ConvertUnits(double value, double factor)
|
||||
{
|
||||
double result = Math.Round(value * factor, 4);
|
||||
return JsonSerializer.Serialize(new { value, factor, result });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Class-Based Agent Skills Sample
|
||||
|
||||
This sample demonstrates how to define **Agent Skills as C# classes** using `AgentClassSkill`.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- Creating skills as classes that extend `AgentClassSkill`
|
||||
- Bundling name, description, body, resources, and scripts into a single class
|
||||
- Using the `AgentSkillsProvider` constructor with class-based skills
|
||||
|
||||
## Skills Included
|
||||
|
||||
### unit-converter (class-based)
|
||||
|
||||
A `UnitConverterSkill` class that converts between common units. Defined in `Program.cs`:
|
||||
|
||||
- `conversion-table` — Static resource with factor table
|
||||
- `convert` — Script that performs `value × factor` conversion
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 10.0 SDK
|
||||
- Azure OpenAI endpoint with a deployed model
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### 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**
|
||||
```
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<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>
|
||||
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
|
||||
</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>
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates an advanced scenario: combining multiple skill types in a single agent
|
||||
// using AgentSkillsProviderBuilder. The builder is designed for cases where the simple
|
||||
// AgentSkillsProvider constructors are insufficient — for example, when you need to mix skill
|
||||
// sources, apply filtering, or configure cross-cutting options in one place.
|
||||
//
|
||||
// Three different skill sources are registered here:
|
||||
// 1. File-based: unit-converter (miles↔km, pounds↔kg) from SKILL.md on disk
|
||||
// 2. Code-defined: volume-converter (gallons↔liters) using AgentInlineSkill
|
||||
// 3. Class-based: temperature-converter (°F↔°C↔K) using AgentClassSkill
|
||||
//
|
||||
// For simpler, single-source scenarios, see the earlier steps in this sample series
|
||||
// (e.g., Step01 for file-based, Step02 for code-defined, Step03 for class-based).
|
||||
|
||||
using System.Text.Json;
|
||||
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";
|
||||
|
||||
// --- 1. Code-Defined Skill: volume-converter ---
|
||||
var volumeConverterSkill = new AgentInlineSkill(
|
||||
name: "volume-converter",
|
||||
description: "Convert between gallons and liters using a multiplication factor.",
|
||||
instructions: """
|
||||
Use this skill when the user asks to convert between gallons and liters.
|
||||
|
||||
1. Review the volume-conversion-table resource to find the correct factor.
|
||||
2. Use the convert-volume script, passing the value and factor.
|
||||
""")
|
||||
.AddResource("volume-conversion-table",
|
||||
"""
|
||||
# Volume Conversion Table
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|---------|---------|---------|
|
||||
| gallons | liters | 3.78541 |
|
||||
| liters | gallons | 0.264172|
|
||||
""")
|
||||
.AddScript("convert-volume", (double value, double factor) =>
|
||||
{
|
||||
double result = Math.Round(value * factor, 4);
|
||||
return JsonSerializer.Serialize(new { value, factor, result });
|
||||
});
|
||||
|
||||
// --- 2. Class-Based Skill: temperature-converter ---
|
||||
var temperatureConverter = new TemperatureConverterSkill();
|
||||
|
||||
// --- 3. Build provider combining all three source types ---
|
||||
var skillsProvider = new AgentSkillsProviderBuilder()
|
||||
.UseFileSkill(Path.Combine(AppContext.BaseDirectory, "skills")) // File-based: unit-converter
|
||||
.UseSkill(volumeConverterSkill) // Code-defined: volume-converter
|
||||
.UseSkill(temperatureConverter) // Class-based: temperature-converter
|
||||
.UseFileScriptRunner(SubprocessScriptRunner.RunAsync)
|
||||
.Build();
|
||||
|
||||
// --- Agent Setup ---
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "MultiConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant that can convert units, volumes, and temperatures.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Example: Use all three skills ---
|
||||
Console.WriteLine("Converting with mixed skills (file + code + class)");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"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?");
|
||||
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
|
||||
/// <summary>
|
||||
/// A temperature-converter skill defined as a C# class.
|
||||
/// </summary>
|
||||
internal sealed class TemperatureConverterSkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new(
|
||||
"temperature-converter",
|
||||
"Convert between temperature scales (Fahrenheit, Celsius, Kelvin).");
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string Instructions => """
|
||||
Use this skill when the user asks to convert temperatures.
|
||||
|
||||
1. Review the temperature-conversion-formulas resource for the correct formula.
|
||||
2. Use the convert-temperature script, passing the value, source scale, and target scale.
|
||||
3. Present the result clearly with both temperature scales.
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
CreateResource(
|
||||
"temperature-conversion-formulas",
|
||||
"""
|
||||
# Temperature Conversion Formulas
|
||||
|
||||
| From | To | Formula |
|
||||
|-------------|-------------|---------------------------|
|
||||
| Fahrenheit | Celsius | °C = (°F − 32) × 5/9 |
|
||||
| Celsius | Fahrenheit | °F = (°C × 9/5) + 32 |
|
||||
| Celsius | Kelvin | K = °C + 273.15 |
|
||||
| Kelvin | Celsius | °C = K − 273.15 |
|
||||
"""),
|
||||
];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
CreateScript("convert-temperature", ConvertTemperature),
|
||||
];
|
||||
|
||||
private static string ConvertTemperature(double value, string from, string to)
|
||||
{
|
||||
double result = (from.ToUpperInvariant(), to.ToUpperInvariant()) switch
|
||||
{
|
||||
("FAHRENHEIT", "CELSIUS") => Math.Round((value - 32) * 5.0 / 9.0, 2),
|
||||
("CELSIUS", "FAHRENHEIT") => Math.Round(value * 9.0 / 5.0 + 32, 2),
|
||||
("CELSIUS", "KELVIN") => Math.Round(value + 273.15, 2),
|
||||
("KELVIN", "CELSIUS") => Math.Round(value - 273.15, 2),
|
||||
_ => throw new ArgumentException($"Unsupported conversion: {from} → {to}")
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(new { value, from, to, result });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Mixed Agent Skills Sample (Advanced)
|
||||
|
||||
This sample demonstrates an **advanced scenario**: combining multiple skill types in a single agent using `AgentSkillsProviderBuilder`.
|
||||
|
||||
> **Tip:** For simpler, single-source scenarios, use the `AgentSkillsProvider` constructors directly — see [Step01](../Agent_Step01_FileBasedSkills/) (file-based), [Step02](../Agent_Step02_CodeDefinedSkills/) (code-defined), or [Step03](../Agent_Step03_ClassBasedSkills/) (class-based).
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- Combining file-based, code-defined, and class-based skills in one provider
|
||||
- Using `UseFileSkill` and `UseSkill` on the builder to register different skill types
|
||||
- Aggregating skills from all sources into a single provider with automatic deduplication
|
||||
|
||||
## When to use `AgentSkillsProviderBuilder`
|
||||
|
||||
The builder is intended for advanced scenarios where the simple `AgentSkillsProvider` constructors are insufficient:
|
||||
|
||||
| Scenario | Builder method |
|
||||
|----------|---------------|
|
||||
| **Mixed skill types** — combine file-based, code-defined, and class-based skills | `UseFileSkill` + `UseSkill` / `UseSkills` |
|
||||
| **Multiple file script runners** — use different script runners for different file skill directories | `UseFileSkill` / `UseFileSkills` with per-source `scriptRunner` |
|
||||
| **Skill filtering** — include/exclude skills using a predicate | `UseFilter(predicate)` |
|
||||
|
||||
## Skills Included
|
||||
|
||||
### unit-converter (file-based)
|
||||
|
||||
Discovered from `skills/unit-converter/SKILL.md` on disk. Converts miles↔km, pounds↔kg.
|
||||
|
||||
### volume-converter (code-defined)
|
||||
|
||||
Defined as `AgentInlineSkill` in `Program.cs`. Converts gallons↔liters.
|
||||
|
||||
### temperature-converter (class-based)
|
||||
|
||||
Defined as `TemperatureConverterSkill` class in `Program.cs`. Converts °F↔°C↔K.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 10.0 SDK
|
||||
- Azure OpenAI endpoint with a deployed model
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Expected 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**
|
||||
```
|
||||
+11
@@ -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/unit-conversion-table.md` to find the correct factor
|
||||
2. Run the `scripts/convert-units.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 |
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Unit conversion script
|
||||
# Converts a value using a multiplication factor: result = value × factor
|
||||
#
|
||||
# Usage:
|
||||
# python scripts/convert-units.py --value 26.2 --factor 1.60934
|
||||
# python scripts/convert-units.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-units.py --value 26.2 --factor 1.60934\n"
|
||||
" python scripts/convert-units.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()
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001;CA1812</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use Dependency Injection (DI) with Agent Skills.
|
||||
// It shows two approaches side-by-side, each handling a different conversion domain:
|
||||
//
|
||||
// 1. Code-defined skill (AgentInlineSkill) — converts distances (miles ↔ kilometers).
|
||||
// Resources and scripts are inline delegates that resolve services from IServiceProvider.
|
||||
//
|
||||
// 2. Class-based skill (AgentClassSkill) — converts weights (pounds ↔ kilograms).
|
||||
// Resources and scripts are encapsulated in a class, also resolving services from IServiceProvider.
|
||||
//
|
||||
// Both skills share the same ConversionService registered in the DI container,
|
||||
// showing that DI works identically regardless of how the skill is defined.
|
||||
// When prompted with a question spanning both domains, the agent uses both skills.
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
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";
|
||||
|
||||
// --- DI Container ---
|
||||
// Register application services that skill resources and scripts can resolve at execution time.
|
||||
ServiceCollection services = new();
|
||||
services.AddSingleton<ConversionService>();
|
||||
|
||||
IServiceProvider serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// =====================================================================
|
||||
// Approach 1: Code-Defined Skill with DI (AgentInlineSkill)
|
||||
// =====================================================================
|
||||
// Handles distance conversions (miles ↔ kilometers).
|
||||
// Resources and scripts are inline delegates. Each delegate can declare
|
||||
// an IServiceProvider parameter that the framework injects automatically.
|
||||
|
||||
var distanceSkill = new AgentInlineSkill(
|
||||
name: "distance-converter",
|
||||
description: "Convert between distance units. Use when asked to convert miles to kilometers or kilometers to miles.",
|
||||
instructions: """
|
||||
Use this skill when the user asks to convert between distance units (miles and kilometers).
|
||||
|
||||
1. Review the distance-table resource to find the factor for the requested conversion.
|
||||
2. Use the convert script, passing the value and factor from the table.
|
||||
""")
|
||||
.AddResource("distance-table", (IServiceProvider serviceProvider) =>
|
||||
{
|
||||
var service = serviceProvider.GetRequiredService<ConversionService>();
|
||||
return service.GetDistanceTable();
|
||||
})
|
||||
.AddScript("convert", (double value, double factor, IServiceProvider serviceProvider) =>
|
||||
{
|
||||
var service = serviceProvider.GetRequiredService<ConversionService>();
|
||||
return service.Convert(value, factor);
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// Approach 2: Class-Based Skill with DI (AgentClassSkill)
|
||||
// =====================================================================
|
||||
// Handles weight conversions (pounds ↔ kilograms).
|
||||
// Resources and scripts are encapsulated in a class. Factory methods
|
||||
// CreateResource and CreateScript accept delegates with IServiceProvider.
|
||||
//
|
||||
// Alternatively, class-based skills can accept dependencies through their
|
||||
// constructor. Register the skill class itself in the ServiceCollection and
|
||||
// resolve it from the container:
|
||||
//
|
||||
// services.AddSingleton<WeightConverterSkill>();
|
||||
// var weightSkill = serviceProvider.GetRequiredService<WeightConverterSkill>();
|
||||
|
||||
var weightSkill = new WeightConverterSkill();
|
||||
|
||||
// --- Skills Provider ---
|
||||
// Both skills are registered with the same provider so the agent can use either one.
|
||||
var skillsProvider = new AgentSkillsProvider(distanceSkill, weightSkill);
|
||||
|
||||
// --- Agent Setup ---
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(
|
||||
options: new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant that can convert units.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName,
|
||||
services: serviceProvider);
|
||||
|
||||
// --- Example: Unit conversion ---
|
||||
// This prompt spans both domains, so the agent will use both skills.
|
||||
Console.WriteLine("Converting units with DI-powered skills");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
|
||||
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class-Based Skill
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// A weight-converter skill defined as a C# class that uses Dependency Injection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This skill resolves <see cref="ConversionService"/> from the DI container
|
||||
/// in both its resource and script functions. This enables clean separation of
|
||||
/// concerns and testability while retaining the class-based skill pattern.
|
||||
/// </remarks>
|
||||
internal sealed class WeightConverterSkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new(
|
||||
"weight-converter",
|
||||
"Convert between weight units. Use when asked to convert pounds to kilograms or kilograms to pounds.");
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string Instructions => """
|
||||
Use this skill when the user asks to convert between weight units (pounds and kilograms).
|
||||
|
||||
1. Review the weight-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.
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
CreateResource("weight-table", (IServiceProvider serviceProvider) =>
|
||||
{
|
||||
var service = serviceProvider.GetRequiredService<ConversionService>();
|
||||
return service.GetWeightTable();
|
||||
}),
|
||||
];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
CreateScript("convert", (double value, double factor, IServiceProvider serviceProvider) =>
|
||||
{
|
||||
var service = serviceProvider.GetRequiredService<ConversionService>();
|
||||
return service.Convert(value, factor);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Services
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Provides conversion rates between units.
|
||||
/// In a real application this could call an external API, read from a database,
|
||||
/// or apply time-varying exchange rates.
|
||||
/// </summary>
|
||||
internal sealed class ConversionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a markdown table of supported distance conversions.
|
||||
/// </summary>
|
||||
public string GetDistanceTable() =>
|
||||
"""
|
||||
# Distance Conversions
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// Returns a markdown table of supported weight conversions.
|
||||
/// </summary>
|
||||
public string GetWeightTable() =>
|
||||
"""
|
||||
# Weight Conversions
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// Converts a value by the given factor and returns a JSON result.
|
||||
/// </summary>
|
||||
public string Convert(double value, double factor)
|
||||
{
|
||||
double result = Math.Round(value * factor, 4);
|
||||
return JsonSerializer.Serialize(new { value, factor, result });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Agent Skills with Dependency Injection
|
||||
|
||||
This sample demonstrates how to use **Dependency Injection (DI)** with Agent Skills. It shows two approaches side-by-side, each handling a different conversion domain:
|
||||
|
||||
1. **Code-defined skill** (`AgentInlineSkill`) — converts **distances** (miles ↔ kilometers)
|
||||
2. **Class-based skill** (`AgentClassSkill`) — converts **weights** (pounds ↔ kilograms)
|
||||
|
||||
Both skills resolve the same `ConversionService` from the DI container. When prompted with a question spanning both domains, the agent uses both skills.
|
||||
|
||||
## What It Shows
|
||||
|
||||
- Registering application services in a `ServiceCollection`
|
||||
- Defining a **code-defined** skill (distance converter) with resources and scripts that resolve services from `IServiceProvider`
|
||||
- Defining a **class-based** skill (weight converter) with resources and scripts that resolve services from `IServiceProvider`
|
||||
- Passing the built `IServiceProvider` to the agent so skills can access DI services at execution time
|
||||
- Running a single prompt that exercises both skills to show they work together
|
||||
|
||||
## How It Works
|
||||
|
||||
1. A `ConversionService` is registered as a singleton in the DI container
|
||||
2. **Code-defined skill**: An `AgentInlineSkill` for distance conversions declares `IServiceProvider` as a parameter in its `AddResource` and `AddScript` delegates — the framework injects it automatically
|
||||
3. **Class-based skill**: A `WeightConverterSkill` class extends `AgentClassSkill` for weight conversions and uses `CreateResource`/`CreateScript` factory methods with `IServiceProvider` parameters
|
||||
4. Both skills resolve `ConversionService` from the provider — one for distance tables, the other for weight tables
|
||||
5. A single agent is created with both skills registered, and the service provider flows through to skill execution
|
||||
|
||||
> **Tip:** Class-based skills can also accept dependencies through their **constructor**. Register the skill class in the `ServiceCollection` and resolve it from the container instead of calling `new` directly. This is useful when the skill itself needs injected services beyond what the resource/script delegates use.
|
||||
|
||||
## How It Differs from Other Samples
|
||||
|
||||
| Sample | Skill Type | DI Support |
|
||||
|--------|------------|------------|
|
||||
| [Step02](../Agent_Step02_CodeDefinedSkills/) | Code-defined (`AgentInlineSkill`) | No — static resources |
|
||||
| [Step03](../Agent_Step03_ClassBasedSkills/) | Class-based (`AgentClassSkill`) | No — static resources |
|
||||
| **Step05 (this)** | **Both code-defined and class-based** | **Yes — DI via `IServiceProvider`** |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10
|
||||
- An Azure OpenAI deployment
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `AZURE_OPENAI_ENDPOINT` | Your Azure OpenAI endpoint URL |
|
||||
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Model deployment name (defaults to `gpt-4o-mini`) |
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
|
||||
```
|
||||
Converting units with DI-powered skills
|
||||
------------------------------------------------------------
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. **26.2 miles → 42.16 km** (a marathon distance)
|
||||
2. **75 kg → 165.35 lbs**
|
||||
```
|
||||
@@ -6,19 +6,32 @@ Samples demonstrating Agent Skills capabilities. Each sample shows a different w
|
||||
|--------|-------------|
|
||||
| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. |
|
||||
| [Agent_Step02_CodeDefinedSkills](Agent_Step02_CodeDefinedSkills/) | Define skills entirely in C# code using `AgentInlineSkill`, with static/dynamic resources and scripts. |
|
||||
| [Agent_Step03_ClassBasedSkills](Agent_Step03_ClassBasedSkills/) | Define skills as C# classes using `AgentClassSkill`. |
|
||||
| [Agent_Step04_MixedSkills](Agent_Step04_MixedSkills/) | **(Advanced)** Combine file-based, code-defined, and class-based skills using `AgentSkillsProviderBuilder`. |
|
||||
| [Agent_Step05_SkillsWithDI](Agent_Step05_SkillsWithDI/) | Use Dependency Injection with both code-defined (`AgentInlineSkill`) and class-based (`AgentClassSkill`) skills. |
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### File-Based vs Code-Defined Skills
|
||||
### Skill Types
|
||||
|
||||
| Aspect | File-Based | Code-Defined |
|
||||
|--------|-----------|--------------|
|
||||
| Definition | `SKILL.md` files on disk | `AgentInlineSkill` instances in C# |
|
||||
| Resources | All files in skill directory (filtered by extension) | `AddResource` (static value or delegate-backed) |
|
||||
| Scripts | Supported via script executor delegate | `AddScript` delegates |
|
||||
| Discovery | Automatic from directory path | Explicit via constructor |
|
||||
| Dynamic content | No (static files only) | Yes (factory delegates) |
|
||||
| Reusability | Copy skill directory | Inline or shared instances |
|
||||
| Aspect | File-Based | Code-Defined | Class-Based |
|
||||
|--------|-----------|--------------|-------------|
|
||||
| Definition | `SKILL.md` files on disk | `AgentInlineSkill` instances in C# | Classes extending `AgentClassSkill` |
|
||||
| Resources | All files in skill directory (filtered by extension) | `AddResource` (static value or delegate-backed) | `CreateResource` factory methods |
|
||||
| Scripts | Supported via script runner delegate | `AddScript` delegates | `CreateScript` factory methods |
|
||||
| Discovery | Automatic from directory path | Explicit via constructor | Explicit via constructor |
|
||||
| Dynamic content | No (static files only) | Yes (factory delegates) | Yes (factory delegates) |
|
||||
| Sharing pattern | Copy skill directory | Inline or shared instances | Package in shared assemblies/NuGet |
|
||||
| DI support | No | Yes (via `IServiceProvider` parameter) | Yes (via `IServiceProvider` parameter) |
|
||||
|
||||
For single-source scenarios, use the `AgentSkillsProvider` constructors directly. To combine multiple skill types, use the `AgentSkillsProviderBuilder`.
|
||||
### `AgentSkillsProvider` vs `AgentSkillsProviderBuilder`
|
||||
|
||||
For single-source scenarios, use the `AgentSkillsProvider` constructors directly — they accept a skill directory path, a set of skills, or a custom source.
|
||||
|
||||
Use `AgentSkillsProviderBuilder` for advanced scenarios where simple constructors are insufficient:
|
||||
|
||||
- **Mixed skill types** — combine file-based, code-defined, and class-based skills in one provider
|
||||
- **Multiple file script runners** — use different script runners for different file skill directories
|
||||
- **Skill filtering** — include or exclude skills using a predicate
|
||||
|
||||
See [Agent_Step04_MixedSkills](Agent_Step04_MixedSkills/) for a working example.
|
||||
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAIW001</NoWarn> <!-- Handoff Orchestrations are Experimental -->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Specialized;
|
||||
@@ -154,6 +155,7 @@ public static partial class AgentWorkflowBuilder
|
||||
/// The <see cref="AIAgent"/> must be capable of understanding those <see cref="AgentRunOptions"/> provided. If the agent
|
||||
/// ignores the tools or is otherwise unable to advertize them to the underlying provider, handoffs will not occur.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
|
||||
public static HandoffWorkflowBuilder CreateHandoffBuilderWith(AIAgent initialAgent)
|
||||
{
|
||||
Throw.IfNull(initialAgent);
|
||||
|
||||
+10
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Workflows.Specialized;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -9,13 +10,21 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
internal static class DiagnosticConstants
|
||||
{
|
||||
public const string ExperimentalFeatureDiagnostic = "MAAIW001";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Prefer HandoffWorkflowBuilder (no 's') instead, which has the same API but the preferred name. This will be removed in a future release before GA.")]
|
||||
#pragma warning disable MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
public sealed class HandoffsWorkflowBuilder(AIAgent initialAgent) : HandoffWorkflowBuilderCore<HandoffsWorkflowBuilder>(initialAgent)
|
||||
#pragma warning restore MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
|
||||
public sealed class HandoffWorkflowBuilder(AIAgent initialAgent) : HandoffWorkflowBuilderCore<HandoffWorkflowBuilder>(initialAgent)
|
||||
{
|
||||
}
|
||||
@@ -23,6 +32,7 @@ public sealed class HandoffWorkflowBuilder(AIAgent initialAgent) : HandoffWorkfl
|
||||
/// <summary>
|
||||
/// Provides a builder for specifying the handoff relationships between agents and building the resulting workflow.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
|
||||
public class HandoffWorkflowBuilderCore<TBuilder> where TBuilder : HandoffWorkflowBuilderCore<TBuilder>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsReleased>true</IsReleased>
|
||||
@@ -8,6 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -31,6 +32,7 @@ internal sealed class HandoffAgentExecutorOptions
|
||||
public HandoffToolCallFilteringBehavior ToolCallFilteringBehavior { get; set; } = HandoffToolCallFilteringBehavior.HandoffOnly;
|
||||
}
|
||||
|
||||
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
|
||||
internal sealed class HandoffMessagesFilter
|
||||
{
|
||||
private readonly HandoffToolCallFilteringBehavior _filteringBehavior;
|
||||
@@ -40,6 +42,7 @@ internal sealed class HandoffMessagesFilter
|
||||
this._filteringBehavior = filteringBehavior;
|
||||
}
|
||||
|
||||
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
|
||||
internal static bool IsHandoffFunctionName(string name)
|
||||
{
|
||||
return name.StartsWith(HandoffWorkflowBuilder.FunctionPrefix, StringComparison.Ordinal);
|
||||
@@ -164,6 +167,7 @@ internal sealed class HandoffMessagesFilter
|
||||
}
|
||||
|
||||
/// <summary>Executor used to represent an agent in a handoffs workflow, responding to <see cref="HandoffState"/> events.</summary>
|
||||
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
|
||||
internal sealed class HandoffAgentExecutor(
|
||||
AIAgent agent,
|
||||
HandoffAgentExecutorOptions options) : Executor<HandoffState, HandoffState>(agent.GetDescriptiveId(), declareCrossRunShareable: true), IResettableExecutor
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
@@ -22,6 +36,20 @@
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
@@ -43,6 +71,20 @@
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
@@ -64,6 +106,20 @@
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
@@ -85,6 +141,20 @@
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -36,6 +37,11 @@ public abstract class AgentSkillScript
|
||||
/// </summary>
|
||||
public string? Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JSON schema describing the parameters accepted by this script, or <see langword="null"/> if not available.
|
||||
/// </summary>
|
||||
public virtual JsonElement? ParametersSchema => null;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the script with the given arguments.
|
||||
/// </summary>
|
||||
|
||||
@@ -117,26 +117,24 @@ public sealed partial class AgentSkillsProvider : AIContextProvider
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class
|
||||
/// with one or more inline (code-defined) skills.
|
||||
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class.
|
||||
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
|
||||
/// </summary>
|
||||
/// <param name="skills">The inline skills to include.</param>
|
||||
public AgentSkillsProvider(params AgentInlineSkill[] skills)
|
||||
: this(skills as IEnumerable<AgentInlineSkill>)
|
||||
/// <param name="skills">The skills to include.</param>
|
||||
public AgentSkillsProvider(params AgentSkill[] skills)
|
||||
: this(skills as IEnumerable<AgentSkill>)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class
|
||||
/// with inline (code-defined) skills.
|
||||
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class.
|
||||
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
|
||||
/// </summary>
|
||||
/// <param name="skills">The inline skills to include.</param>
|
||||
/// <param name="skills">The skills to include.</param>
|
||||
/// <param name="options">Optional provider configuration.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory.</param>
|
||||
public AgentSkillsProvider(
|
||||
IEnumerable<AgentInlineSkill> skills,
|
||||
IEnumerable<AgentSkill> skills,
|
||||
AgentSkillsProviderOptions? options = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
: this(
|
||||
|
||||
@@ -11,15 +11,31 @@ namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Fluent builder for constructing an <see cref="AgentSkillsProvider"/> backed by a composite source.
|
||||
/// Intended for advanced scenarios where the simple <see cref="AgentSkillsProvider"/> constructors are insufficient.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Use this builder to combine multiple skill sources into a single provider:
|
||||
/// For simple, single-source scenarios, prefer the <see cref="AgentSkillsProvider"/> constructors directly
|
||||
/// (e.g., passing a skill directory path or a set of skills). Use this builder when you need one or more
|
||||
/// of the following advanced capabilities:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><strong>Mixed skill types</strong> — combine file-based, code-defined (<see cref="AgentInlineSkill"/>),
|
||||
/// and class-based (<see cref="AgentClassSkill"/>) skills in a single provider.</description></item>
|
||||
/// <item><description><strong>Multiple file script runners</strong> — use different script runners for different
|
||||
/// file skill directories via per-source <c>scriptRunner</c> parameters on
|
||||
/// <see cref="UseFileSkill"/> / <see cref="UseFileSkills(IEnumerable{string}, AgentFileSkillsSourceOptions?, AgentFileSkillScriptRunner?)"/>.</description></item>
|
||||
/// <item><description><strong>Skill filtering</strong> — include or exclude skills using a predicate
|
||||
/// via <see cref="UseFilter"/>.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Example — combining file-based and code-defined skills:
|
||||
/// </para>
|
||||
/// <code>
|
||||
/// var provider = new AgentSkillsProviderBuilder()
|
||||
/// .UseFileSkills("/path/to/skills")
|
||||
/// .UseSkills(myInlineSkill1, myInlineSkill2)
|
||||
/// .UseFileScriptRunner(SubprocessScriptRunner.RunAsync)
|
||||
/// .Build();
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for defining skills as C# classes that bundle all components together.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Inherit from this class to create a self-contained skill definition. Override the abstract
|
||||
/// properties to provide name, description, and instructions. Use <see cref="CreateResource(string, object, string?)"/>,
|
||||
/// <see cref="CreateResource(string, Delegate, string?)"/>, and <see cref="CreateScript"/> to define
|
||||
/// inline resources and scripts.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public class PdfFormatterSkill : AgentClassSkill
|
||||
/// {
|
||||
/// private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
/// private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
///
|
||||
/// public override AgentSkillFrontmatter Frontmatter { get; } = new("pdf-formatter", "Format documents as PDF.");
|
||||
/// protected override string Instructions => "Use this skill to format documents...";
|
||||
///
|
||||
/// public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
/// [
|
||||
/// CreateResource("template", "Use this template..."),
|
||||
/// ];
|
||||
///
|
||||
/// public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
/// [
|
||||
/// CreateScript("format-pdf", FormatPdf),
|
||||
/// ];
|
||||
///
|
||||
/// private static string FormatPdf(string content) => content;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public abstract class AgentClassSkill : AgentSkill
|
||||
{
|
||||
private string? _content;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw instructions text for this skill.
|
||||
/// </summary>
|
||||
protected abstract string Instructions { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Returns a synthesized XML document containing name, description, instructions, resources, and scripts.
|
||||
/// The result is cached after the first access. Override to provide custom content.
|
||||
/// </remarks>
|
||||
public override string Content => this._content ??= AgentInlineSkillContentBuilder.Build(
|
||||
this.Frontmatter.Name,
|
||||
this.Frontmatter.Description,
|
||||
this.Instructions,
|
||||
this.Resources,
|
||||
this.Scripts);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a skill resource backed by a static value.
|
||||
/// </summary>
|
||||
/// <param name="name">The resource name.</param>
|
||||
/// <param name="value">The static resource value.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
/// <returns>A new <see cref="AgentSkillResource"/> instance.</returns>
|
||||
protected static AgentSkillResource CreateResource(string name, object value, string? description = null)
|
||||
=> new AgentInlineSkillResource(name, value, description);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a skill resource backed by a delegate that produces a dynamic value.
|
||||
/// </summary>
|
||||
/// <param name="name">The resource name.</param>
|
||||
/// <param name="method">A method that produces the resource value when requested.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
/// <returns>A new <see cref="AgentSkillResource"/> instance.</returns>
|
||||
protected static AgentSkillResource CreateResource(string name, Delegate method, string? description = null)
|
||||
=> new AgentInlineSkillResource(name, method, description);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a skill script backed by a delegate.
|
||||
/// </summary>
|
||||
/// <param name="name">The script name.</param>
|
||||
/// <param name="method">A method to execute when the script is invoked.</param>
|
||||
/// <param name="description">An optional description of the script.</param>
|
||||
/// <returns>A new <see cref="AgentSkillScript"/> instance.</returns>
|
||||
protected static AgentSkillScript CreateScript(string name, Delegate method, string? description = null)
|
||||
=> new AgentInlineSkillScript(name, method, description);
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
@@ -27,8 +25,8 @@ namespace Microsoft.Agents.AI;
|
||||
public sealed class AgentInlineSkill : AgentSkill
|
||||
{
|
||||
private readonly string _instructions;
|
||||
private List<AgentSkillResource>? _resources;
|
||||
private List<AgentSkillScript>? _scripts;
|
||||
private List<AgentInlineSkillResource>? _resources;
|
||||
private List<AgentInlineSkillScript>? _scripts;
|
||||
private string? _cachedContent;
|
||||
|
||||
/// <summary>
|
||||
@@ -77,7 +75,7 @@ public sealed class AgentInlineSkill : AgentSkill
|
||||
public override AgentSkillFrontmatter Frontmatter { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Content => this._cachedContent ??= this.BuildContent();
|
||||
public override string Content => this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._resources, this._scripts);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources;
|
||||
@@ -125,91 +123,4 @@ public sealed class AgentInlineSkill : AgentSkill
|
||||
(this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description));
|
||||
return this;
|
||||
}
|
||||
|
||||
private string BuildContent()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append($"<name>{EscapeXmlString(this.Frontmatter.Name)}</name>\n")
|
||||
.Append($"<description>{EscapeXmlString(this.Frontmatter.Description)}</description>\n\n")
|
||||
.Append("<instructions>\n")
|
||||
.Append(EscapeXmlString(this._instructions))
|
||||
.Append("\n</instructions>");
|
||||
|
||||
if (this.Resources is { Count: > 0 })
|
||||
{
|
||||
sb.Append("\n\n<resources>\n");
|
||||
foreach (var resource in this.Resources)
|
||||
{
|
||||
if (resource.Description is not null)
|
||||
{
|
||||
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\" description=\"{EscapeXmlString(resource.Description)}\"/>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\"/>\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</resources>");
|
||||
}
|
||||
|
||||
if (this.Scripts is { Count: > 0 })
|
||||
{
|
||||
sb.Append("\n\n<scripts>\n");
|
||||
foreach (var script in this.Scripts)
|
||||
{
|
||||
JsonElement? parametersSchema = ((AgentInlineSkillScript)script).ParametersSchema;
|
||||
|
||||
if (script.Description is null && parametersSchema is null)
|
||||
{
|
||||
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(script.Description is not null
|
||||
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
|
||||
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");
|
||||
|
||||
if (parametersSchema is not null)
|
||||
{
|
||||
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
|
||||
}
|
||||
|
||||
sb.Append(" </script>\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</scripts>");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes XML special characters: always escapes <c>&</c>, <c><</c>, <c>></c>,
|
||||
/// <c>"</c>, and <c>'</c>. When <paramref name="preserveQuotes"/> is <see langword="true"/>,
|
||||
/// quotes are left unescaped to preserve readability of embedded content such as JSON.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to escape.</param>
|
||||
/// <param name="preserveQuotes">
|
||||
/// When <see langword="true"/>, leaves <c>"</c> and <c>'</c> unescaped for use in XML element content (e.g., JSON).
|
||||
/// When <see langword="false"/> (default), escapes all XML special characters including quotes.
|
||||
/// </param>
|
||||
private static string EscapeXmlString(string value, bool preserveQuotes = false)
|
||||
{
|
||||
var result = value
|
||||
.Replace("&", "&")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">");
|
||||
|
||||
if (!preserveQuotes)
|
||||
{
|
||||
result = result
|
||||
.Replace("\"", """)
|
||||
.Replace("'", "'");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper that builds XML-structured content strings for code-defined and class-based skills.
|
||||
/// </summary>
|
||||
internal static class AgentInlineSkillContentBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the complete skill content containing name, description, instructions, resources, and scripts.
|
||||
/// </summary>
|
||||
/// <param name="name">The skill name.</param>
|
||||
/// <param name="description">The skill description.</param>
|
||||
/// <param name="instructions">The raw instructions text.</param>
|
||||
/// <param name="resources">Optional resources associated with the skill.</param>
|
||||
/// <param name="scripts">Optional scripts associated with the skill.</param>
|
||||
/// <returns>An XML-structured content string.</returns>
|
||||
public static string Build(
|
||||
string name,
|
||||
string description,
|
||||
string instructions,
|
||||
IReadOnlyList<AgentSkillResource>? resources,
|
||||
IReadOnlyList<AgentSkillScript>? scripts)
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(name);
|
||||
_ = Throw.IfNullOrWhitespace(description);
|
||||
_ = Throw.IfNullOrWhitespace(instructions);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append($"<name>{EscapeXmlString(name)}</name>\n")
|
||||
.Append($"<description>{EscapeXmlString(description)}</description>\n\n")
|
||||
.Append("<instructions>\n")
|
||||
.Append(EscapeXmlString(instructions))
|
||||
.Append("\n</instructions>");
|
||||
|
||||
if (resources is { Count: > 0 })
|
||||
{
|
||||
sb.Append("\n\n<resources>\n");
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
if (resource.Description is not null)
|
||||
{
|
||||
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\" description=\"{EscapeXmlString(resource.Description)}\"/>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\"/>\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</resources>");
|
||||
}
|
||||
|
||||
if (scripts is { Count: > 0 })
|
||||
{
|
||||
sb.Append("\n\n<scripts>\n");
|
||||
foreach (var script in scripts)
|
||||
{
|
||||
var parametersSchema = script.ParametersSchema;
|
||||
|
||||
if (script.Description is null && parametersSchema is null)
|
||||
{
|
||||
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(script.Description is not null
|
||||
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
|
||||
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");
|
||||
|
||||
if (parametersSchema is not null)
|
||||
{
|
||||
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
|
||||
}
|
||||
|
||||
sb.Append(" </script>\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</scripts>");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes XML special characters: always escapes <c>&</c>, <c><</c>, <c>></c>,
|
||||
/// <c>"</c>, and <c>'</c>. When <paramref name="preserveQuotes"/> is <see langword="true"/>,
|
||||
/// quotes are left unescaped to preserve readability of embedded content such as JSON.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to escape.</param>
|
||||
/// <param name="preserveQuotes">
|
||||
/// When <see langword="true"/>, leaves <c>"</c> and <c>'</c> unescaped for use in XML element content (e.g., JSON).
|
||||
/// When <see langword="false"/> (default), escapes all XML special characters including quotes.
|
||||
/// </param>
|
||||
private static string EscapeXmlString(string value, bool preserveQuotes = false)
|
||||
{
|
||||
var result = value
|
||||
.Replace("&", "&")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">");
|
||||
|
||||
if (!preserveQuotes)
|
||||
{
|
||||
result = result
|
||||
.Replace("\"", """)
|
||||
.Replace("'", "'");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ internal sealed class AgentInlineSkillScript : AgentSkillScript
|
||||
/// <summary>
|
||||
/// Gets the JSON schema describing the parameters accepted by this script, or <see langword="null"/> if not available.
|
||||
/// </summary>
|
||||
public JsonElement? ParametersSchema => this._function.JsonSchema;
|
||||
public override JsonElement? ParametersSchema => this._function.JsonSchema;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AgentClassSkill"/> and <see cref="AgentInMemorySkillsSource"/>.
|
||||
/// </summary>
|
||||
public sealed class AgentClassSkillTests
|
||||
{
|
||||
[Fact]
|
||||
public void Resources_DefaultsToNull_WhenNotOverridden()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new MinimalClassSkill();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Null(skill.Resources);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scripts_DefaultsToNull_WhenNotOverridden()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new MinimalClassSkill();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Null(skill.Scripts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resources_ReturnsOverriddenList_WhenOverridden()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new FullClassSkill();
|
||||
|
||||
// Act
|
||||
var resources = skill.Resources;
|
||||
|
||||
// Assert
|
||||
Assert.Single(resources!);
|
||||
Assert.Equal("test-resource", resources![0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scripts_ReturnsOverriddenList_WhenOverridden()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new FullClassSkill();
|
||||
|
||||
// Act
|
||||
var scripts = skill.Scripts;
|
||||
|
||||
// Assert
|
||||
Assert.Single(scripts!);
|
||||
Assert.Equal("TestScript", scripts![0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResourcesAndScripts_CanBeLazyLoaded_AndCached()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new LazyLoadedSkill();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Equal(0, skill.ResourceCreationCount);
|
||||
Assert.Equal(0, skill.ScriptCreationCount);
|
||||
|
||||
var firstResources = skill.Resources;
|
||||
var firstScripts = skill.Scripts;
|
||||
var secondResources = skill.Resources;
|
||||
var secondScripts = skill.Scripts;
|
||||
|
||||
Assert.Single(firstResources!);
|
||||
Assert.Single(firstScripts!);
|
||||
Assert.Same(firstResources, secondResources);
|
||||
Assert.Same(firstScripts, secondScripts);
|
||||
Assert.Equal(1, skill.ResourceCreationCount);
|
||||
Assert.Equal(1, skill.ScriptCreationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_Content_ReturnClassDefinedValues()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new MinimalClassSkill();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Equal("minimal", skill.Frontmatter.Name);
|
||||
Assert.Contains("<instructions>", skill.Content);
|
||||
Assert.Contains("Minimal skill body.", skill.Content);
|
||||
Assert.Contains("</instructions>", skill.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_ReturnsSynthesizedXmlDocument()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new MinimalClassSkill();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Contains("<name>minimal</name>", skill.Content);
|
||||
Assert.Contains("<description>A minimal skill.</description>", skill.Content);
|
||||
Assert.Contains("<instructions>", skill.Content);
|
||||
Assert.Contains("Minimal skill body.", skill.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AgentInMemorySkillsSource_ReturnsAllSkillsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skills = new AgentClassSkill[] { new MinimalClassSkill(), new FullClassSkill() };
|
||||
var source = new AgentInMemorySkillsSource(skills);
|
||||
|
||||
// Act
|
||||
var result = await source.GetSkillsAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("minimal", result[0].Frontmatter.Name);
|
||||
Assert.Equal("full", result[1].Frontmatter.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgentClassSkill_InvalidFrontmatter_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() => new AgentSkillFrontmatter("INVALID-NAME", "An invalid skill."));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkillWithOnlyResources_HasNullScripts()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new ResourceOnlySkill();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Single(skill.Resources!);
|
||||
Assert.Null(skill.Scripts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkillWithOnlyScripts_HasNullResources()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new ScriptOnlySkill();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Null(skill.Resources);
|
||||
Assert.Single(skill.Scripts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_ReturnsCachedInstance_OnRepeatedAccess()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new FullClassSkill();
|
||||
|
||||
// Act
|
||||
var first = skill.Content;
|
||||
var second = skill.Content;
|
||||
|
||||
// Assert
|
||||
Assert.Same(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_IncludesParametersSchema_WhenScriptsHaveParameters()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new FullClassSkill();
|
||||
|
||||
// Act
|
||||
var content = skill.Content;
|
||||
|
||||
// Assert — scripts with typed parameters should have their schema included
|
||||
Assert.Contains("parameters_schema", content);
|
||||
Assert.Contains("value", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_IncludesDerivedResources_WhenResourcesUseBaseTypeOverrides()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new DerivedResourceSkill();
|
||||
|
||||
// Act
|
||||
var content = skill.Content;
|
||||
|
||||
// Assert
|
||||
Assert.Contains("<resources>", content);
|
||||
Assert.Contains("custom-resource", content);
|
||||
Assert.Contains("Custom resource description.", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_IncludesDerivedScripts_WhenScriptsUseBaseTypeOverrides()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new DerivedScriptSkill();
|
||||
|
||||
// Act
|
||||
var content = skill.Content;
|
||||
|
||||
// Assert
|
||||
Assert.Contains("<scripts>", content);
|
||||
Assert.Contains("custom-script", content);
|
||||
Assert.Contains("Custom script description.", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Content_OmitsParametersSchema_WhenDerivedScriptDoesNotProvideOne()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new DerivedScriptSkill();
|
||||
|
||||
// Act
|
||||
var content = skill.Content;
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain("parameters_schema", content);
|
||||
}
|
||||
|
||||
#region Test skill classes
|
||||
|
||||
private sealed class MinimalClassSkill : AgentClassSkill
|
||||
{
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new("minimal", "A minimal skill.");
|
||||
|
||||
protected override string Instructions => "Minimal skill body.";
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => null;
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
|
||||
}
|
||||
|
||||
private sealed class FullClassSkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new("full", "A full skill with resources and scripts.");
|
||||
|
||||
protected override string Instructions => "Full skill body.";
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
CreateResource("test-resource", "resource content"),
|
||||
];
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
CreateScript("TestScript", TestScript),
|
||||
];
|
||||
|
||||
private static string TestScript(double value) =>
|
||||
JsonSerializer.Serialize(new { result = value * 2 });
|
||||
}
|
||||
|
||||
private sealed class ResourceOnlySkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new("resource-only", "Skill with resources only.");
|
||||
|
||||
protected override string Instructions => "Body.";
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
CreateResource("data", "some data"),
|
||||
];
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
|
||||
}
|
||||
|
||||
private sealed class ScriptOnlySkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new("script-only", "Skill with scripts only.");
|
||||
|
||||
protected override string Instructions => "Body.";
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => null;
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
CreateScript("ToUpper", (string input) => input.ToUpperInvariant()),
|
||||
];
|
||||
}
|
||||
|
||||
private sealed class DerivedResourceSkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new("derived-resource", "Skill with a derived resource type.");
|
||||
|
||||
protected override string Instructions => "Body.";
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
new CustomResource("custom-resource", "Custom resource description."),
|
||||
];
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
|
||||
}
|
||||
|
||||
private sealed class DerivedScriptSkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new("derived-script", "Skill with a derived script type.");
|
||||
|
||||
protected override string Instructions => "Body.";
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => null;
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
new CustomScript("custom-script", "Custom script description."),
|
||||
];
|
||||
}
|
||||
|
||||
private sealed class LazyLoadedSkill : AgentClassSkill
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new("lazy-loaded", "Skill with lazily created resources and scripts.");
|
||||
|
||||
protected override string Instructions => "Body.";
|
||||
|
||||
public int ResourceCreationCount { get; private set; }
|
||||
|
||||
public int ScriptCreationCount { get; private set; }
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??= this.CreateResources();
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??= this.CreateScripts();
|
||||
|
||||
private IReadOnlyList<AgentSkillResource> CreateResources()
|
||||
{
|
||||
this.ResourceCreationCount++;
|
||||
return [CreateResource("lazy-resource", "resource content")];
|
||||
}
|
||||
|
||||
private IReadOnlyList<AgentSkillScript> CreateScripts()
|
||||
{
|
||||
this.ScriptCreationCount++;
|
||||
return [CreateScript("LazyScript", () => "done")];
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CustomResource : AgentSkillResource
|
||||
{
|
||||
public CustomResource(string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
}
|
||||
|
||||
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<object?>("resource-value");
|
||||
}
|
||||
|
||||
private sealed class CustomScript : AgentSkillScript
|
||||
{
|
||||
public CustomScript(string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
}
|
||||
|
||||
public override Task<object?> RunAsync(AgentSkill skill, Extensions.AI.AIFunctionArguments arguments, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<object?>("script-result");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -851,6 +851,61 @@ public sealed class AgentSkillsProviderTests : IDisposable
|
||||
Assert.Contains("First instructions.", content!.ToString()!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Constructor_ClassSkillsParams_ProvidesSkillsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new TestClassSkill("class-a", "Class A", "Class instructions.");
|
||||
var provider = new AgentSkillsProvider(skill);
|
||||
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
|
||||
|
||||
// Act
|
||||
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.Instructions);
|
||||
Assert.Contains("class-a", result.Instructions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Constructor_ClassSkillsEnumerable_ProvidesSkillsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skills = new List<AgentClassSkill>
|
||||
{
|
||||
new TestClassSkill("enum-class-a", "Class A", "Instructions A."),
|
||||
new TestClassSkill("enum-class-b", "Class B", "Instructions B."),
|
||||
};
|
||||
var provider = new AgentSkillsProvider(skills);
|
||||
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
|
||||
|
||||
// Act
|
||||
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.Instructions);
|
||||
Assert.Contains("enum-class-a", result.Instructions);
|
||||
Assert.Contains("enum-class-b", result.Instructions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Constructor_ClassSkills_DeduplicatesAsync()
|
||||
{
|
||||
// Arrange — two class skills with the same name
|
||||
var skill1 = new TestClassSkill("dup-class", "First", "First instructions.");
|
||||
var skill2 = new TestClassSkill("dup-class", "Second", "Second instructions.");
|
||||
var provider = new AgentSkillsProvider(skill1, skill2);
|
||||
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
|
||||
|
||||
// Act
|
||||
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
var loadSkillTool = result.Tools!.First(t => t.Name == "load_skill") as AIFunction;
|
||||
var content = await loadSkillTool!.InvokeAsync(new AIFunctionArguments(new Dictionary<string, object?> { ["skillName"] = "dup-class" }));
|
||||
|
||||
// Assert — only first occurrence survives
|
||||
Assert.Contains("First instructions.", content!.ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A test skill source that counts how many times <see cref="GetSkillsAsync"/> is called.
|
||||
/// </summary>
|
||||
@@ -872,4 +927,23 @@ public sealed class AgentSkillsProviderTests : IDisposable
|
||||
return Task.FromResult(this._skills);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestClassSkill : AgentClassSkill
|
||||
{
|
||||
private readonly string _instructions;
|
||||
|
||||
public TestClassSkill(string name, string description, string instructions)
|
||||
{
|
||||
this.Frontmatter = new AgentSkillFrontmatter(name, description);
|
||||
this._instructions = instructions;
|
||||
}
|
||||
|
||||
public override AgentSkillFrontmatter Frontmatter { get; }
|
||||
|
||||
protected override string Instructions => this._instructions;
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => null;
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<NoWarn>$(NoWarn);MEAI001;MAAIW001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user