From 3c1288bf9202a04f5a93591c203dd6b72795b627 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:51:52 +0000 Subject: [PATCH 1/7] add class-based skills --- dotnet/agent-framework-dotnet.slnx | 4 + dotnet/filtered-unit.slnx | 342 ++++++++++++++++ .../Agent_Step01_FileBasedSkills/README.md | 2 +- .../Agent_Step03_ClassBasedSkills.csproj | 21 + .../Agent_Step03_ClassBasedSkills/Program.cs | 102 +++++ .../Agent_Step03_ClassBasedSkills/README.md | 49 +++ .../Agent_Step04_MixedSkills.csproj | 32 ++ .../Agent_Step04_MixedSkills/Program.cs | 149 +++++++ .../Agent_Step04_MixedSkills/README.md | 67 +++ .../skills/unit-converter/SKILL.md | 11 + .../references/unit-conversion-table.md | 10 + .../unit-converter/scripts/convert-units.py | 29 ++ ...gent_Step05_CodeDefinedSkillsWithDI.csproj | 22 + .../Program.cs | 116 ++++++ .../README.md | 38 ++ ...Agent_Step06_ClassBasedSkillsWithDI.csproj | 22 + .../Program.cs | 140 +++++++ .../README.md | 58 +++ .../samples/02-agents/AgentSkills/README.md | 34 +- .../Skills/AgentSkillScript.cs | 6 + .../Skills/AgentSkillsProvider.cs | 16 +- .../Skills/AgentSkillsProviderBuilder.cs | 18 +- .../Skills/Programmatic/AgentClassSkill.cs | 95 +++++ .../Skills/Programmatic/AgentInlineSkill.cs | 95 +---- .../AgentInlineSkillContentBuilder.cs | 118 ++++++ .../Programmatic/AgentInlineSkillScript.cs | 2 +- .../AgentSkills/AgentClassSkillTests.cs | 382 ++++++++++++++++++ .../AgentSkills/AgentSkillsProviderTests.cs | 74 ++++ .../DeduplicatingAgentSkillsSourceTests.cs | 8 +- .../FilteringAgentSkillsSourceTests.cs | 10 +- 30 files changed, 1949 insertions(+), 123 deletions(-) create mode 100644 dotnet/filtered-unit.slnx create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/README.md create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/SKILL.md create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/references/unit-conversion-table.md create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/scripts/convert-units.py create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Agent_Step05_CodeDefinedSkillsWithDI.csproj create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Program.cs create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/README.md create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Agent_Step06_ClassBasedSkillsWithDI.csproj create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Program.cs create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/README.md create mode 100644 dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs create mode 100644 dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index b9755dac83..f81fd55d58 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -106,6 +106,10 @@ + + + + diff --git a/dotnet/filtered-unit.slnx b/dotnet/filtered-unit.slnx new file mode 100644 index 0000000000..1a3ad7e6b2 --- /dev/null +++ b/dotnet/filtered-unit.slnx @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/README.md index 41b813b98f..592aca4a27 100644 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/README.md +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/README.md @@ -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 diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj new file mode 100644 index 0000000000..fd3d71fe7e --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + + enable + enable + $(NoWarn);MAAI001 + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs new file mode 100644 index 0000000000..fb0f202230 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs @@ -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}"); + +/// +/// A unit-converter skill defined as a C# class. +/// +/// +/// Class-based skills bundle all components (name, description, body, resources, scripts) +/// into a single class. +/// +internal sealed class UnitConverterSkill : AgentClassSkill +{ + private IReadOnlyList? _resources; + private IReadOnlyList? _scripts; + + /// + 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."); + + /// + 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. + """; + + /// + public override IReadOnlyList? 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 | + """), + ]; + + /// + public override IReadOnlyList? 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 }); + } +} diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md new file mode 100644 index 0000000000..506784256a --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md @@ -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** +``` diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj new file mode 100644 index 0000000000..7e7e9ef0fa --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj @@ -0,0 +1,32 @@ + + + + Exe + net10.0 + + enable + enable + $(NoWarn);MAAI001 + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs new file mode 100644 index 0000000000..b8a9e8fbb1 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs @@ -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}"); + +/// +/// A temperature-converter skill defined as a C# class. +/// +internal sealed class TemperatureConverterSkill : AgentClassSkill +{ + private IReadOnlyList? _resources; + private IReadOnlyList? _scripts; + + /// + public override AgentSkillFrontmatter Frontmatter { get; } = new( + "temperature-converter", + "Convert between temperature scales (Fahrenheit, Celsius, Kelvin)."); + + /// + 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. + """; + + /// + public override IReadOnlyList? 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 | + """), + ]; + + /// + public override IReadOnlyList? 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 }); + } +} diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/README.md new file mode 100644 index 0000000000..14a0d089b9 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/README.md @@ -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** +``` diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/SKILL.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/SKILL.md new file mode 100644 index 0000000000..246a3392f7 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/SKILL.md @@ -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 --factor ` (e.g. `--value 26.2 --factor 1.60934`) +3. Present the converted value clearly with both units diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/references/unit-conversion-table.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/references/unit-conversion-table.md new file mode 100644 index 0000000000..7a0160b854 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/references/unit-conversion-table.md @@ -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 | diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/scripts/convert-units.py b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/scripts/convert-units.py new file mode 100644 index 0000000000..ac271dd594 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/skills/unit-converter/scripts/convert-units.py @@ -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() diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Agent_Step05_CodeDefinedSkillsWithDI.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Agent_Step05_CodeDefinedSkillsWithDI.csproj new file mode 100644 index 0000000000..959fa29167 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Agent_Step05_CodeDefinedSkillsWithDI.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + + enable + enable + $(NoWarn);MAAI001;CA1812 + + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Program.cs new file mode 100644 index 0000000000..9424b1acaa --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Program.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to use Dependency Injection (DI) with Agent Skills. +// Skill script and resource functions can resolve services from the DI container via +// IServiceProvider, enabling clean separation of concerns and testability. +// +// The sample registers a ConversionRateService in the DI container. A code-defined skill +// resource resolves this service to list supported conversions dynamically, and a skill +// script resolves it to look up live conversion rates at execution time. + +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"; + +// --- Build the code-defined skill --- +// The skill uses DI to resolve ConversionRateService in both its resource and script functions. +var unitConverterSkill = new AgentInlineSkill( + name: "unit-converter", + description: "Convert between common units. Use when asked to convert miles, kilometers, pounds, or kilograms.", + 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. Check the conversion-policy resource for rounding and formatting rules. + 3. Use the convert script, passing the value and factor from the table. + """) + // Dynamic resource with DI: resolves ConversionRateService to build conversion table + .AddResource("conversion-table", (IServiceProvider serviceProvider) => + { + var rateService = serviceProvider.GetRequiredService(); + return rateService.GetConversionTable(); + }) + // Script with DI: resolves ConversionRateService to perform the conversion + .AddScript("convert", (double value, double factor, IServiceProvider serviceProvider) => + { + var rateService = serviceProvider.GetRequiredService(); + return rateService.Convert(value, factor); + }); + +// --- Skills Provider --- +var skillsProvider = new AgentSkillsProvider(unitConverterSkill); + +// --- DI Container --- +// Register application services that skill scripts can resolve at execution time. +ServiceCollection services = new(); +services.AddSingleton(); + +// --- 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: services.BuildServiceProvider()); + +// --- Example: Unit conversion --- +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}"); + +// --------------------------------------------------------------------------- +// Services +// --------------------------------------------------------------------------- + +/// +/// 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. +/// +internal sealed class ConversionRateService +{ + /// + /// Returns a static markdown table of all supported conversions with factors. + /// + public string GetConversionTable() => + """ + # 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 | + """; + + /// + /// Converts a value by the given factor and returns a JSON result. + /// + public string Convert(double value, double factor) + { + double result = Math.Round(value * factor, 4); + return JsonSerializer.Serialize(new { value, factor, result }); + } +} diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/README.md new file mode 100644 index 0000000000..aa4281ca00 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/README.md @@ -0,0 +1,38 @@ +# Agent Skills with Dependency Injection + +This sample demonstrates how to use **Dependency Injection (DI)** with Agent Skills resources and script functions. + +## What It Shows + +- Registering application services in a `ServiceCollection` +- Defining a code-defined skill resource that resolves services from `IServiceProvider` +- Defining a code-defined skill script that resolves services from `IServiceProvider` +- Passing the built `IServiceProvider` to the agent so skills can access DI services at execution time + +## How It Works + +1. A `ConversionRateService` is registered as a singleton in the DI container +2. A code-defined skill resource declares `IServiceProvider` as a parameter — the framework injects it automatically +3. The resource resolves `ConversionRateService` from the provider to build a supported-conversions table dynamically +4. A code-defined skill script also declares `IServiceProvider` as a parameter to look up conversion factors at runtime +5. The agent is created with the service provider, which flows through to skill resource and script execution + +## 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 +``` diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Agent_Step06_ClassBasedSkillsWithDI.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Agent_Step06_ClassBasedSkillsWithDI.csproj new file mode 100644 index 0000000000..959fa29167 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Agent_Step06_ClassBasedSkillsWithDI.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + + enable + enable + $(NoWarn);MAAI001;CA1812 + + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Program.cs new file mode 100644 index 0000000000..08044ddfc7 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Program.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to use Dependency Injection (DI) with class-based Agent Skills. +// Unlike code-defined skills (Step05), class-based skills bundle all components into a single +// class extending AgentClassSkill. Skill script and resource functions can still resolve +// services from the DI container via IServiceProvider, combining class-based organization +// with the flexibility of DI. + +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"; + +// --- Class-Based Skill with DI --- +// Instantiate the skill class. Its resources and scripts will resolve services from +// the DI container at execution time. +var unitConverter = new UnitConverterSkill(); + +// --- Skills Provider --- +var skillsProvider = new AgentSkillsProvider(unitConverter); + +// --- DI Container --- +// Register application services that skill scripts can resolve at execution time. +ServiceCollection services = new(); +services.AddSingleton(); + +// --- 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: services.BuildServiceProvider()); + +// --- Example: Unit conversion --- +Console.WriteLine("Converting units with DI-powered 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}"); + +/// +/// A unit-converter skill defined as a C# class that uses Dependency Injection. +/// +/// +/// This skill resolves 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. +/// +internal sealed class UnitConverterSkill : AgentClassSkill +{ + private IReadOnlyList? _resources; + private IReadOnlyList? _scripts; + + /// + 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."); + + /// + 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. + """; + + /// + public override IReadOnlyList? Resources => this._resources ??= + [ + // Dynamic resource with DI: resolves ConversionRateService to build conversion table + CreateResource("conversion-table", (IServiceProvider serviceProvider) => + { + var rateService = serviceProvider.GetRequiredService(); + return rateService.GetConversionTable(); + }), + ]; + + /// + public override IReadOnlyList? Scripts => this._scripts ??= + [ + // Script with DI: resolves ConversionRateService to perform the conversion + CreateScript("convert", (double value, double factor, IServiceProvider serviceProvider) => + { + var rateService = serviceProvider.GetRequiredService(); + return rateService.Convert(value, factor); + }), + ]; +} + +/// +/// 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. +/// +internal sealed class ConversionRateService +{ + /// + /// Returns a static markdown table of all supported conversions with factors. + /// + public string GetConversionTable() => + """ + # 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 | + """; + + /// + /// Converts a value by the given factor and returns a JSON result. + /// + public string Convert(double value, double factor) + { + double result = Math.Round(value * factor, 4); + return JsonSerializer.Serialize(new { value, factor, result }); + } +} diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/README.md new file mode 100644 index 0000000000..1406dbb309 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/README.md @@ -0,0 +1,58 @@ +# Class-Based Agent Skills with Dependency Injection + +This sample demonstrates how to use **Dependency Injection (DI)** with **class-based Agent Skills** (`AgentClassSkill`). + +## What It Shows + +- Defining a skill as a class that extends `AgentClassSkill` +- Using `IServiceProvider` in skill resource delegates to resolve services from the DI container +- Using `IServiceProvider` in skill script delegates to resolve services from the DI container +- Registering application services in a `ServiceCollection` and passing the built provider to the agent + +## How It Works + +1. A `ConversionRateService` is registered as a singleton in the DI container +2. `UnitConverterSkill` extends `AgentClassSkill` and declares its resources and scripts using `CreateResource` and `CreateScript` factory methods +3. The resource delegate declares `IServiceProvider` as a parameter — the framework injects it automatically +4. The resource resolves `ConversionRateService` from the provider to build a supported-conversions table dynamically +5. The script delegate also declares `IServiceProvider` as a parameter to look up conversion factors at runtime +6. The agent is created with the service provider, which flows through to skill resource and script execution + +## How It Differs from Other Samples + +| Sample | Skill Type | DI Support | +|--------|-----------|------------| +| [Step03](../Agent_Step03_ClassBasedSkills/) | Class-based (`AgentClassSkill`) | No — static resources | +| [Step05](../Agent_Step05_CodeDefinedSkillsWithDI/) | Code-defined (`AgentInlineSkill`) | Yes — inline delegates | +| **Step06 (this)** | **Class-based (`AgentClassSkill`)** | **Yes — class delegates** | + +## 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 class-based skills +------------------------------------------------------------ +Agent: Here are your conversions: + +1. **26.2 miles → 42.16 km** (a marathon distance) +2. **75 kg → 165.35 lbs** +``` diff --git a/dotnet/samples/02-agents/AgentSkills/README.md b/dotnet/samples/02-agents/AgentSkills/README.md index 6011384997..bcdcf6be81 100644 --- a/dotnet/samples/02-agents/AgentSkills/README.md +++ b/dotnet/samples/02-agents/AgentSkills/README.md @@ -6,19 +6,33 @@ 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_CodeDefinedSkillsWithDI](Agent_Step05_CodeDefinedSkillsWithDI/) | Use Dependency Injection with code-defined skills (`AgentInlineSkill`). | +| [Agent_Step06_ClassBasedSkillsWithDI](Agent_Step06_ClassBasedSkillsWithDI/) | Use Dependency Injection with class-based skills (`AgentClassSkill`). | ## 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. diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillScript.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillScript.cs index ad647d2eb0..1ac44bfac8 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillScript.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillScript.cs @@ -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 @@ protected AgentSkillScript(string name, string? description = null) /// public string? Description { get; } + /// + /// Gets the JSON schema describing the parameters accepted by this script, or if not available. + /// + public virtual JsonElement? ParametersSchema => null; + /// /// Runs the script with the given arguments. /// diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs index 70d7939227..b5598e19d3 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs @@ -117,26 +117,24 @@ public AgentSkillsProvider( } /// - /// Initializes a new instance of the class - /// with one or more inline (code-defined) skills. + /// Initializes a new instance of the class. /// Duplicate skill names are automatically deduplicated (first occurrence wins). /// - /// The inline skills to include. - public AgentSkillsProvider(params AgentInlineSkill[] skills) - : this(skills as IEnumerable) + /// The skills to include. + public AgentSkillsProvider(params AgentSkill[] skills) + : this(skills as IEnumerable) { } /// - /// Initializes a new instance of the class - /// with inline (code-defined) skills. + /// Initializes a new instance of the class. /// Duplicate skill names are automatically deduplicated (first occurrence wins). /// - /// The inline skills to include. + /// The skills to include. /// Optional provider configuration. /// Optional logger factory. public AgentSkillsProvider( - IEnumerable skills, + IEnumerable skills, AgentSkillsProviderOptions? options = null, ILoggerFactory? loggerFactory = null) : this( diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs index 8e52cc522e..0da54d0426 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs @@ -11,15 +11,31 @@ namespace Microsoft.Agents.AI; /// /// Fluent builder for constructing an backed by a composite source. +/// Intended for advanced scenarios where the simple constructors are insufficient. /// /// /// -/// Use this builder to combine multiple skill sources into a single provider: +/// For simple, single-source scenarios, prefer the 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: +/// +/// +/// Mixed skill types — combine file-based, code-defined (), +/// and class-based () skills in a single provider. +/// Multiple file script runners — use different script runners for different +/// file skill directories via per-source scriptRunner parameters on +/// / . +/// Skill filtering — include or exclude skills using a predicate +/// via . +/// +/// +/// Example — combining file-based and code-defined skills: /// /// /// var provider = new AgentSkillsProviderBuilder() /// .UseFileSkills("/path/to/skills") /// .UseSkills(myInlineSkill1, myInlineSkill2) +/// .UseFileScriptRunner(SubprocessScriptRunner.RunAsync) /// .Build(); /// /// diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs new file mode 100644 index 0000000000..47f12d5ea5 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Agents.AI; + +/// +/// Abstract base class for defining skills as C# classes that bundle all components together. +/// +/// +/// +/// Inherit from this class to create a self-contained skill definition. Override the abstract +/// properties to provide name, description, and instructions. Use , +/// , and to define +/// inline resources and scripts. +/// +/// +/// +/// +/// 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; +/// } +/// +/// +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] +public abstract class AgentClassSkill : AgentSkill +{ + private string? _content; + + /// + /// Gets the raw instructions text for this skill. + /// + protected abstract string Instructions { get; } + + /// + /// + /// 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. + /// + public override string Content => this._content ??= AgentInlineSkillContentBuilder.Build( + this.Frontmatter.Name, + this.Frontmatter.Description, + this.Instructions, + this.Resources, + this.Scripts); + + /// + /// Creates a skill resource backed by a static value. + /// + /// The resource name. + /// The static resource value. + /// An optional description of the resource. + /// A new instance. + protected static AgentSkillResource CreateResource(string name, object value, string? description = null) + => new AgentInlineSkillResource(name, value, description); + + /// + /// Creates a skill resource backed by a delegate that produces a dynamic value. + /// + /// The resource name. + /// A method that produces the resource value when requested. + /// An optional description of the resource. + /// A new instance. + protected static AgentSkillResource CreateResource(string name, Delegate method, string? description = null) + => new AgentInlineSkillResource(name, method, description); + + /// + /// Creates a skill script backed by a delegate. + /// + /// The script name. + /// A method to execute when the script is invoked. + /// An optional description of the script. + /// A new instance. + protected static AgentSkillScript CreateScript(string name, Delegate method, string? description = null) + => new AgentInlineSkillScript(name, method, description); +} diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs index d326a47a59..f24d41c362 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs @@ -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? _resources; - private List? _scripts; + private List? _resources; + private List? _scripts; private string? _cachedContent; /// @@ -77,7 +75,7 @@ public AgentInlineSkill( public override AgentSkillFrontmatter Frontmatter { get; } /// - 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); /// public override IReadOnlyList? Resources => this._resources; @@ -125,91 +123,4 @@ public AgentInlineSkill AddScript(string name, Delegate method, string? descript (this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description)); return this; } - - private string BuildContent() - { - var sb = new StringBuilder(); - - sb.Append($"{EscapeXmlString(this.Frontmatter.Name)}\n") - .Append($"{EscapeXmlString(this.Frontmatter.Description)}\n\n") - .Append("\n") - .Append(EscapeXmlString(this._instructions)) - .Append("\n"); - - if (this.Resources is { Count: > 0 }) - { - sb.Append("\n\n\n"); - foreach (var resource in this.Resources) - { - if (resource.Description is not null) - { - sb.Append($" \n"); - } - else - { - sb.Append($" \n"); - } - } - - sb.Append(""); - } - - if (this.Scripts is { Count: > 0 }) - { - sb.Append("\n\n\n"); - foreach (var script in this.Scripts) - { - JsonElement? parametersSchema = ((AgentInlineSkillScript)script).ParametersSchema; - - if (script.Description is null && parametersSchema is null) - { - sb.Append($" \n"); - } - } - - sb.Append(""); - } - - return sb.ToString(); - } - - /// - /// Escapes XML special characters: always escapes &, <, >, - /// ", and '. When is , - /// quotes are left unescaped to preserve readability of embedded content such as JSON. - /// - /// The string to escape. - /// - /// When , leaves " and ' unescaped for use in XML element content (e.g., JSON). - /// When (default), escapes all XML special characters including quotes. - /// - private static string EscapeXmlString(string value, bool preserveQuotes = false) - { - var result = value - .Replace("&", "&") - .Replace("<", "<") - .Replace(">", ">"); - - if (!preserveQuotes) - { - result = result - .Replace("\"", """) - .Replace("'", "'"); - } - - return result; - } } diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs new file mode 100644 index 0000000000..f90d67dd1d --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs @@ -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; + +/// +/// Internal helper that builds XML-structured content strings for code-defined and class-based skills. +/// +internal static class AgentInlineSkillContentBuilder +{ + /// + /// Builds the complete skill content containing name, description, instructions, resources, and scripts. + /// + /// The skill name. + /// The skill description. + /// The raw instructions text. + /// Optional resources associated with the skill. + /// Optional scripts associated with the skill. + /// An XML-structured content string. + public static string Build( + string name, + string description, + string instructions, + IReadOnlyList? resources, + IReadOnlyList? scripts) + { + _ = Throw.IfNullOrWhitespace(name); + _ = Throw.IfNullOrWhitespace(description); + _ = Throw.IfNullOrWhitespace(instructions); + + var sb = new StringBuilder(); + + sb.Append($"{EscapeXmlString(name)}\n") + .Append($"{EscapeXmlString(description)}\n\n") + .Append("\n") + .Append(EscapeXmlString(instructions)) + .Append("\n"); + + if (resources is { Count: > 0 }) + { + sb.Append("\n\n\n"); + foreach (var resource in resources) + { + if (resource.Description is not null) + { + sb.Append($" \n"); + } + else + { + sb.Append($" \n"); + } + } + + sb.Append(""); + } + + if (scripts is { Count: > 0 }) + { + sb.Append("\n\n\n"); + foreach (var script in scripts) + { + var parametersSchema = script.ParametersSchema; + + if (script.Description is null && parametersSchema is null) + { + sb.Append($" \n"); + } + } + + sb.Append(""); + } + + return sb.ToString(); + } + + /// + /// Escapes XML special characters: always escapes &, <, >, + /// ", and '. When is , + /// quotes are left unescaped to preserve readability of embedded content such as JSON. + /// + /// The string to escape. + /// + /// When , leaves " and ' unescaped for use in XML element content (e.g., JSON). + /// When (default), escapes all XML special characters including quotes. + /// + private static string EscapeXmlString(string value, bool preserveQuotes = false) + { + var result = value + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">"); + + if (!preserveQuotes) + { + result = result + .Replace("\"", """) + .Replace("'", "'"); + } + + return result; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs index acb4f4780b..e851c6f9fc 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs @@ -36,7 +36,7 @@ public AgentInlineSkillScript(string name, Delegate method, string? description /// /// Gets the JSON schema describing the parameters accepted by this script, or if not available. /// - public JsonElement? ParametersSchema => this._function.JsonSchema; + public override JsonElement? ParametersSchema => this._function.JsonSchema; /// public override async Task RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default) diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs new file mode 100644 index 0000000000..cc70611160 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs @@ -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; + +/// +/// Unit tests for and . +/// +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("", skill.Content); + Assert.Contains("Minimal skill body.", skill.Content); + Assert.Contains("", skill.Content); + } + + [Fact] + public void Content_ReturnsSynthesizedXmlDocument() + { + // Arrange + var skill = new MinimalClassSkill(); + + // Act & Assert + Assert.Contains("minimal", skill.Content); + Assert.Contains("A minimal skill.", skill.Content); + Assert.Contains("", skill.Content); + Assert.Contains("Minimal skill body.", skill.Content); + } + + [Fact] + public async Task AgentInMemorySkillsSource_ReturnsAllSkills() + { + // 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(() => 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("", 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("", 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? Resources => null; + + public override IReadOnlyList? Scripts => null; + } + + private sealed class FullClassSkill : AgentClassSkill + { + private IReadOnlyList? _resources; + private IReadOnlyList? _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? Resources => this._resources ??= + [ + CreateResource("test-resource", "resource content"), + ]; + + public override IReadOnlyList? 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? _resources; + + public override AgentSkillFrontmatter Frontmatter { get; } = new("resource-only", "Skill with resources only."); + + protected override string Instructions => "Body."; + + public override IReadOnlyList? Resources => this._resources ??= + [ + CreateResource("data", "some data"), + ]; + + public override IReadOnlyList? Scripts => null; + } + + private sealed class ScriptOnlySkill : AgentClassSkill + { + private IReadOnlyList? _scripts; + + public override AgentSkillFrontmatter Frontmatter { get; } = new("script-only", "Skill with scripts only."); + + protected override string Instructions => "Body."; + + public override IReadOnlyList? Resources => null; + + public override IReadOnlyList? Scripts => this._scripts ??= + [ + CreateScript("ToUpper", (string input) => input.ToUpperInvariant()), + ]; + } + + private sealed class DerivedResourceSkill : AgentClassSkill + { + private IReadOnlyList? _resources; + + public override AgentSkillFrontmatter Frontmatter { get; } = new("derived-resource", "Skill with a derived resource type."); + + protected override string Instructions => "Body."; + + public override IReadOnlyList? Resources => this._resources ??= + [ + new CustomResource("custom-resource", "Custom resource description."), + ]; + + public override IReadOnlyList? Scripts => null; + } + + private sealed class DerivedScriptSkill : AgentClassSkill + { + private IReadOnlyList? _scripts; + + public override AgentSkillFrontmatter Frontmatter { get; } = new("derived-script", "Skill with a derived script type."); + + protected override string Instructions => "Body."; + + public override IReadOnlyList? Resources => null; + + public override IReadOnlyList? Scripts => this._scripts ??= + [ + new CustomScript("custom-script", "Custom script description."), + ]; + } + + private sealed class LazyLoadedSkill : AgentClassSkill + { + private IReadOnlyList? _resources; + private IReadOnlyList? _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? Resources => this._resources ??= this.CreateResources(); + + public override IReadOnlyList? Scripts => this._scripts ??= this.CreateScripts(); + + private IReadOnlyList CreateResources() + { + this.ResourceCreationCount++; + return [CreateResource("lazy-resource", "resource content")]; + } + + private IReadOnlyList 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 ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default) + => Task.FromResult("resource-value"); + } + + private sealed class CustomScript : AgentSkillScript + { + public CustomScript(string name, string? description = null) + : base(name, description) + { + } + + public override Task RunAsync(AgentSkill skill, Extensions.AI.AIFunctionArguments arguments, CancellationToken cancellationToken = default) + => Task.FromResult("script-result"); + } + + #endregion +} diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs index 87e98b3da3..e86eb0894a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs @@ -851,6 +851,61 @@ public async Task Constructor_InlineSkills_DeduplicatesAsync() 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 + { + 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 { ["skillName"] = "dup-class" })); + + // Assert — only first occurrence survives + Assert.Contains("First instructions.", content!.ToString()!); + } + /// /// A test skill source that counts how many times is called. /// @@ -872,4 +927,23 @@ public override Task> GetSkillsAsync(CancellationToken cancell 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? Resources => null; + + public override IReadOnlyList? Scripts => null; + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs index 7895023681..0ee6af9b3b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills; public sealed class DeduplicatingAgentSkillsSourceTests { [Fact] - public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkillsAsync() + public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkills() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] @@ -31,7 +31,7 @@ public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkillsAsync() } [Fact] - public async Task GetSkillsAsync_WithDuplicates_KeepsFirstOccurrenceAsync() + public async Task GetSkillsAsync_WithDuplicates_KeepsFirstOccurrence() { // Arrange var skills = new AgentSkill[] @@ -53,7 +53,7 @@ public async Task GetSkillsAsync_WithDuplicates_KeepsFirstOccurrenceAsync() } [Fact] - public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirstAsync() + public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirst() { // Arrange - Use a custom source that returns skills with same name but different casing var inner = new FakeDuplicateCaseSource(); @@ -68,7 +68,7 @@ public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirstAsync() } [Fact] - public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() + public async Task GetSkillsAsync_EmptySource_ReturnsEmpty() { // Arrange var inner = new AgentInMemorySkillsSource(System.Array.Empty()); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs index 12bdb28e05..b24ef2e1b1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills; public sealed class FilteringAgentSkillsSourceTests { [Fact] - public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkillsAsync() + public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkills() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] @@ -30,7 +30,7 @@ public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkillsAsync() } [Fact] - public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmptyAsync() + public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmpty() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] @@ -48,7 +48,7 @@ public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmptyAsync() } [Fact] - public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnlyAsync() + public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnly() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] @@ -70,7 +70,7 @@ public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnlyAsync() } [Fact] - public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() + public async Task GetSkillsAsync_EmptySource_ReturnsEmpty() { // Arrange var inner = new AgentInMemorySkillsSource(Array.Empty()); @@ -101,7 +101,7 @@ public void Constructor_NullInnerSource_Throws() } [Fact] - public async Task GetSkillsAsync_PreservesOrderAsync() + public async Task GetSkillsAsync_PreservesOrder() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] From 90932309d4053af6c4a993d4bbfacd8859110b43 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:34:22 +0000 Subject: [PATCH 2/7] address formating issues --- dotnet/AGENTS.md | 2 +- .../AgentSkills/AgentClassSkillTests.cs | 2 +- .../AgentSkills/DeduplicatingAgentSkillsSourceTests.cs | 8 ++++---- .../AgentSkills/FilteringAgentSkillsSourceTests.cs | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dotnet/AGENTS.md b/dotnet/AGENTS.md index 4cb4b67e5f..50f10c3e69 100644 --- a/dotnet/AGENTS.md +++ b/dotnet/AGENTS.md @@ -35,7 +35,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 diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs index cc70611160..2c453f21b6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs @@ -111,7 +111,7 @@ public void Content_ReturnsSynthesizedXmlDocument() } [Fact] - public async Task AgentInMemorySkillsSource_ReturnsAllSkills() + public async Task AgentInMemorySkillsSource_ReturnsAllSkillsAsync() { // Arrange var skills = new AgentClassSkill[] { new MinimalClassSkill(), new FullClassSkill() }; diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs index 0ee6af9b3b..7895023681 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/DeduplicatingAgentSkillsSourceTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills; public sealed class DeduplicatingAgentSkillsSourceTests { [Fact] - public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkills() + public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkillsAsync() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] @@ -31,7 +31,7 @@ public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkills() } [Fact] - public async Task GetSkillsAsync_WithDuplicates_KeepsFirstOccurrence() + public async Task GetSkillsAsync_WithDuplicates_KeepsFirstOccurrenceAsync() { // Arrange var skills = new AgentSkill[] @@ -53,7 +53,7 @@ public async Task GetSkillsAsync_WithDuplicates_KeepsFirstOccurrence() } [Fact] - public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirst() + public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirstAsync() { // Arrange - Use a custom source that returns skills with same name but different casing var inner = new FakeDuplicateCaseSource(); @@ -68,7 +68,7 @@ public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirst() } [Fact] - public async Task GetSkillsAsync_EmptySource_ReturnsEmpty() + public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() { // Arrange var inner = new AgentInMemorySkillsSource(System.Array.Empty()); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs index b24ef2e1b1..12bdb28e05 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FilteringAgentSkillsSourceTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills; public sealed class FilteringAgentSkillsSourceTests { [Fact] - public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkills() + public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkillsAsync() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] @@ -30,7 +30,7 @@ public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkills() } [Fact] - public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmpty() + public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmptyAsync() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] @@ -48,7 +48,7 @@ public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmpty() } [Fact] - public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnly() + public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnlyAsync() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] @@ -70,7 +70,7 @@ public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnly() } [Fact] - public async Task GetSkillsAsync_EmptySource_ReturnsEmpty() + public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() { // Arrange var inner = new AgentInMemorySkillsSource(Array.Empty()); @@ -101,7 +101,7 @@ public void Constructor_NullInnerSource_Throws() } [Fact] - public async Task GetSkillsAsync_PreservesOrder() + public async Task GetSkillsAsync_PreservesOrderAsync() { // Arrange var inner = new AgentInMemorySkillsSource(new AgentSkill[] From 1cff7bffb61c85d0d127383b5672289dc6bebb0e Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:58:11 +0000 Subject: [PATCH 3/7] Remove generated filtered-unit.slnx and add to .gitignore The filtered solution file is generated dynamically by eng/scripts/New-FilteredSolution.ps1 during CI. Checking it in risks it becoming stale and out-of-sync with the real solution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/filtered-unit.slnx | 342 -------------------------------------- 1 file changed, 342 deletions(-) delete mode 100644 dotnet/filtered-unit.slnx diff --git a/dotnet/filtered-unit.slnx b/dotnet/filtered-unit.slnx deleted file mode 100644 index 1a3ad7e6b2..0000000000 --- a/dotnet/filtered-unit.slnx +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 78863498178330a3d19b3170778c97b63ce813e8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:58:11 +0000 Subject: [PATCH 4/7] Remove generated filtered-unit.slnx and add to .gitignore The filtered solution file is generated dynamically by eng/scripts/New-FilteredSolution.ps1 during CI. Checking it in risks it becoming stale and out-of-sync with the real solution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 3 + dotnet/filtered-unit.slnx | 342 -------------------------------------- 2 files changed, 3 insertions(+), 342 deletions(-) delete mode 100644 dotnet/filtered-unit.slnx diff --git a/.gitignore b/.gitignore index 4dd5848e89..089abb5395 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/dotnet/filtered-unit.slnx b/dotnet/filtered-unit.slnx deleted file mode 100644 index 1a3ad7e6b2..0000000000 --- a/dotnet/filtered-unit.slnx +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 4b6094bfe58cd05aa7180631607f38c7e3e1ce95 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:59:19 +0000 Subject: [PATCH 5/7] consolidate DI samples into one --- dotnet/agent-framework-dotnet.slnx | 3 +- .../Program.cs | 116 ---------- .../README.md | 38 ---- .../Agent_Step05_SkillsWithDI.csproj} | 0 .../Agent_Step05_SkillsWithDI/Program.cs | 208 ++++++++++++++++++ .../Agent_Step05_SkillsWithDI/README.md | 65 ++++++ ...Agent_Step06_ClassBasedSkillsWithDI.csproj | 22 -- .../Program.cs | 140 ------------ .../README.md | 58 ----- .../samples/02-agents/AgentSkills/README.md | 3 +- 10 files changed, 275 insertions(+), 378 deletions(-) delete mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Program.cs delete mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/README.md rename dotnet/samples/02-agents/AgentSkills/{Agent_Step05_CodeDefinedSkillsWithDI/Agent_Step05_CodeDefinedSkillsWithDI.csproj => Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj} (100%) create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs create mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/README.md delete mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Agent_Step06_ClassBasedSkillsWithDI.csproj delete mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Program.cs delete mode 100644 dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/README.md diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 5d8b72587f..88dceab1b1 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -109,8 +109,7 @@ - - + diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Program.cs deleted file mode 100644 index 9424b1acaa..0000000000 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Program.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample demonstrates how to use Dependency Injection (DI) with Agent Skills. -// Skill script and resource functions can resolve services from the DI container via -// IServiceProvider, enabling clean separation of concerns and testability. -// -// The sample registers a ConversionRateService in the DI container. A code-defined skill -// resource resolves this service to list supported conversions dynamically, and a skill -// script resolves it to look up live conversion rates at execution time. - -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"; - -// --- Build the code-defined skill --- -// The skill uses DI to resolve ConversionRateService in both its resource and script functions. -var unitConverterSkill = new AgentInlineSkill( - name: "unit-converter", - description: "Convert between common units. Use when asked to convert miles, kilometers, pounds, or kilograms.", - 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. Check the conversion-policy resource for rounding and formatting rules. - 3. Use the convert script, passing the value and factor from the table. - """) - // Dynamic resource with DI: resolves ConversionRateService to build conversion table - .AddResource("conversion-table", (IServiceProvider serviceProvider) => - { - var rateService = serviceProvider.GetRequiredService(); - return rateService.GetConversionTable(); - }) - // Script with DI: resolves ConversionRateService to perform the conversion - .AddScript("convert", (double value, double factor, IServiceProvider serviceProvider) => - { - var rateService = serviceProvider.GetRequiredService(); - return rateService.Convert(value, factor); - }); - -// --- Skills Provider --- -var skillsProvider = new AgentSkillsProvider(unitConverterSkill); - -// --- DI Container --- -// Register application services that skill scripts can resolve at execution time. -ServiceCollection services = new(); -services.AddSingleton(); - -// --- 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: services.BuildServiceProvider()); - -// --- Example: Unit conversion --- -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}"); - -// --------------------------------------------------------------------------- -// Services -// --------------------------------------------------------------------------- - -/// -/// 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. -/// -internal sealed class ConversionRateService -{ - /// - /// Returns a static markdown table of all supported conversions with factors. - /// - public string GetConversionTable() => - """ - # 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 | - """; - - /// - /// Converts a value by the given factor and returns a JSON result. - /// - public string Convert(double value, double factor) - { - double result = Math.Round(value * factor, 4); - return JsonSerializer.Serialize(new { value, factor, result }); - } -} diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/README.md deleted file mode 100644 index aa4281ca00..0000000000 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Agent Skills with Dependency Injection - -This sample demonstrates how to use **Dependency Injection (DI)** with Agent Skills resources and script functions. - -## What It Shows - -- Registering application services in a `ServiceCollection` -- Defining a code-defined skill resource that resolves services from `IServiceProvider` -- Defining a code-defined skill script that resolves services from `IServiceProvider` -- Passing the built `IServiceProvider` to the agent so skills can access DI services at execution time - -## How It Works - -1. A `ConversionRateService` is registered as a singleton in the DI container -2. A code-defined skill resource declares `IServiceProvider` as a parameter — the framework injects it automatically -3. The resource resolves `ConversionRateService` from the provider to build a supported-conversions table dynamically -4. A code-defined skill script also declares `IServiceProvider` as a parameter to look up conversion factors at runtime -5. The agent is created with the service provider, which flows through to skill resource and script execution - -## 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 -``` diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Agent_Step05_CodeDefinedSkillsWithDI.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj similarity index 100% rename from dotnet/samples/02-agents/AgentSkills/Agent_Step05_CodeDefinedSkillsWithDI/Agent_Step05_CodeDefinedSkillsWithDI.csproj rename to dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs new file mode 100644 index 0000000000..eedbbbda41 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs @@ -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(); + +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(); + return service.GetDistanceTable(); + }) + .AddScript("convert", (double value, double factor, IServiceProvider serviceProvider) => + { + var service = serviceProvider.GetRequiredService(); + 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(); +// var weightSkill = serviceProvider.GetRequiredService(); + +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 +// --------------------------------------------------------------------------- + +/// +/// A weight-converter skill defined as a C# class that uses Dependency Injection. +/// +/// +/// This skill resolves 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. +/// +internal sealed class WeightConverterSkill : AgentClassSkill +{ + private IReadOnlyList? _resources; + private IReadOnlyList? _scripts; + + /// + public override AgentSkillFrontmatter Frontmatter { get; } = new( + "weight-converter", + "Convert between weight units. Use when asked to convert pounds to kilograms or kilograms to pounds."); + + /// + 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. + """; + + /// + public override IReadOnlyList? Resources => this._resources ??= + [ + CreateResource("weight-table", (IServiceProvider serviceProvider) => + { + var service = serviceProvider.GetRequiredService(); + return service.GetWeightTable(); + }), + ]; + + /// + public override IReadOnlyList? Scripts => this._scripts ??= + [ + CreateScript("convert", (double value, double factor, IServiceProvider serviceProvider) => + { + var service = serviceProvider.GetRequiredService(); + return service.Convert(value, factor); + }), + ]; +} + +// --------------------------------------------------------------------------- +// Services +// --------------------------------------------------------------------------- + +/// +/// 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. +/// +internal sealed class ConversionService +{ + /// + /// Returns a markdown table of supported distance conversions. + /// + public string GetDistanceTable() => + """ + # Distance Conversions + + Formula: **result = value × factor** + + | From | To | Factor | + |-------------|-------------|----------| + | miles | kilometers | 1.60934 | + | kilometers | miles | 0.621371 | + """; + + /// + /// Returns a markdown table of supported weight conversions. + /// + public string GetWeightTable() => + """ + # Weight Conversions + + Formula: **result = value × factor** + + | From | To | Factor | + |-------------|-------------|----------| + | pounds | kilograms | 0.453592 | + | kilograms | pounds | 2.20462 | + """; + + /// + /// Converts a value by the given factor and returns a JSON result. + /// + public string Convert(double value, double factor) + { + double result = Math.Round(value * factor, 4); + return JsonSerializer.Serialize(new { value, factor, result }); + } +} diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/README.md new file mode 100644 index 0000000000..b284b745d5 --- /dev/null +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/README.md @@ -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** +``` diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Agent_Step06_ClassBasedSkillsWithDI.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Agent_Step06_ClassBasedSkillsWithDI.csproj deleted file mode 100644 index 959fa29167..0000000000 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Agent_Step06_ClassBasedSkillsWithDI.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - Exe - net10.0 - - enable - enable - $(NoWarn);MAAI001;CA1812 - - - - - - - - - - - - - diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Program.cs deleted file mode 100644 index 08044ddfc7..0000000000 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/Program.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample demonstrates how to use Dependency Injection (DI) with class-based Agent Skills. -// Unlike code-defined skills (Step05), class-based skills bundle all components into a single -// class extending AgentClassSkill. Skill script and resource functions can still resolve -// services from the DI container via IServiceProvider, combining class-based organization -// with the flexibility of DI. - -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"; - -// --- Class-Based Skill with DI --- -// Instantiate the skill class. Its resources and scripts will resolve services from -// the DI container at execution time. -var unitConverter = new UnitConverterSkill(); - -// --- Skills Provider --- -var skillsProvider = new AgentSkillsProvider(unitConverter); - -// --- DI Container --- -// Register application services that skill scripts can resolve at execution time. -ServiceCollection services = new(); -services.AddSingleton(); - -// --- 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: services.BuildServiceProvider()); - -// --- Example: Unit conversion --- -Console.WriteLine("Converting units with DI-powered 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}"); - -/// -/// A unit-converter skill defined as a C# class that uses Dependency Injection. -/// -/// -/// This skill resolves 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. -/// -internal sealed class UnitConverterSkill : AgentClassSkill -{ - private IReadOnlyList? _resources; - private IReadOnlyList? _scripts; - - /// - 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."); - - /// - 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. - """; - - /// - public override IReadOnlyList? Resources => this._resources ??= - [ - // Dynamic resource with DI: resolves ConversionRateService to build conversion table - CreateResource("conversion-table", (IServiceProvider serviceProvider) => - { - var rateService = serviceProvider.GetRequiredService(); - return rateService.GetConversionTable(); - }), - ]; - - /// - public override IReadOnlyList? Scripts => this._scripts ??= - [ - // Script with DI: resolves ConversionRateService to perform the conversion - CreateScript("convert", (double value, double factor, IServiceProvider serviceProvider) => - { - var rateService = serviceProvider.GetRequiredService(); - return rateService.Convert(value, factor); - }), - ]; -} - -/// -/// 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. -/// -internal sealed class ConversionRateService -{ - /// - /// Returns a static markdown table of all supported conversions with factors. - /// - public string GetConversionTable() => - """ - # 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 | - """; - - /// - /// Converts a value by the given factor and returns a JSON result. - /// - public string Convert(double value, double factor) - { - double result = Math.Round(value * factor, 4); - return JsonSerializer.Serialize(new { value, factor, result }); - } -} diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/README.md deleted file mode 100644 index 1406dbb309..0000000000 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step06_ClassBasedSkillsWithDI/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Class-Based Agent Skills with Dependency Injection - -This sample demonstrates how to use **Dependency Injection (DI)** with **class-based Agent Skills** (`AgentClassSkill`). - -## What It Shows - -- Defining a skill as a class that extends `AgentClassSkill` -- Using `IServiceProvider` in skill resource delegates to resolve services from the DI container -- Using `IServiceProvider` in skill script delegates to resolve services from the DI container -- Registering application services in a `ServiceCollection` and passing the built provider to the agent - -## How It Works - -1. A `ConversionRateService` is registered as a singleton in the DI container -2. `UnitConverterSkill` extends `AgentClassSkill` and declares its resources and scripts using `CreateResource` and `CreateScript` factory methods -3. The resource delegate declares `IServiceProvider` as a parameter — the framework injects it automatically -4. The resource resolves `ConversionRateService` from the provider to build a supported-conversions table dynamically -5. The script delegate also declares `IServiceProvider` as a parameter to look up conversion factors at runtime -6. The agent is created with the service provider, which flows through to skill resource and script execution - -## How It Differs from Other Samples - -| Sample | Skill Type | DI Support | -|--------|-----------|------------| -| [Step03](../Agent_Step03_ClassBasedSkills/) | Class-based (`AgentClassSkill`) | No — static resources | -| [Step05](../Agent_Step05_CodeDefinedSkillsWithDI/) | Code-defined (`AgentInlineSkill`) | Yes — inline delegates | -| **Step06 (this)** | **Class-based (`AgentClassSkill`)** | **Yes — class delegates** | - -## 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 class-based skills ------------------------------------------------------------- -Agent: Here are your conversions: - -1. **26.2 miles → 42.16 km** (a marathon distance) -2. **75 kg → 165.35 lbs** -``` diff --git a/dotnet/samples/02-agents/AgentSkills/README.md b/dotnet/samples/02-agents/AgentSkills/README.md index bcdcf6be81..bbf511da4e 100644 --- a/dotnet/samples/02-agents/AgentSkills/README.md +++ b/dotnet/samples/02-agents/AgentSkills/README.md @@ -8,8 +8,7 @@ Samples demonstrating Agent Skills capabilities. Each sample shows a different w | [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_CodeDefinedSkillsWithDI](Agent_Step05_CodeDefinedSkillsWithDI/) | Use Dependency Injection with code-defined skills (`AgentInlineSkill`). | -| [Agent_Step06_ClassBasedSkillsWithDI](Agent_Step06_ClassBasedSkillsWithDI/) | Use Dependency Injection with class-based skills (`AgentClassSkill`). | +| [Agent_Step05_SkillsWithDI](Agent_Step05_SkillsWithDI/) | Use Dependency Injection with both code-defined (`AgentInlineSkill`) and class-based (`AgentClassSkill`) skills. | ## Key Concepts From 56a625aa6c089b03df91a351530543075e40dda1 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:50:36 +0000 Subject: [PATCH 6/7] fix file encoding --- .../02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs index eedbbbda41..f0c5a4c8a5 100644 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// 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: From b758b3cfa26de23fae93d83575c52ef819849b24 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:12:29 +0000 Subject: [PATCH 7/7] suppress compatibility warning --- .../CompatibilitySuppressions.xml | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI/CompatibilitySuppressions.xml b/dotnet/src/Microsoft.Agents.AI/CompatibilitySuppressions.xml index ba6315295e..8b7fd65d4c 100644 --- a/dotnet/src/Microsoft.Agents.AI/CompatibilitySuppressions.xml +++ b/dotnet/src/Microsoft.Agents.AI/CompatibilitySuppressions.xml @@ -1,6 +1,20 @@  + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[]) + lib/net10.0/Microsoft.Agents.AI.dll + lib/net10.0/Microsoft.Agents.AI.dll + true + + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory) + lib/net10.0/Microsoft.Agents.AI.dll + lib/net10.0/Microsoft.Agents.AI.dll + true + CP0002 M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory @@ -22,6 +36,20 @@ lib/net10.0/Microsoft.Agents.AI.dll true + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[]) + lib/net472/Microsoft.Agents.AI.dll + lib/net472/Microsoft.Agents.AI.dll + true + + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory) + lib/net472/Microsoft.Agents.AI.dll + lib/net472/Microsoft.Agents.AI.dll + true + CP0002 M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory @@ -43,6 +71,20 @@ lib/net472/Microsoft.Agents.AI.dll true + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[]) + lib/net8.0/Microsoft.Agents.AI.dll + lib/net8.0/Microsoft.Agents.AI.dll + true + + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory) + lib/net8.0/Microsoft.Agents.AI.dll + lib/net8.0/Microsoft.Agents.AI.dll + true + CP0002 M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory @@ -64,6 +106,20 @@ lib/net8.0/Microsoft.Agents.AI.dll true + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[]) + lib/net9.0/Microsoft.Agents.AI.dll + lib/net9.0/Microsoft.Agents.AI.dll + true + + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory) + lib/net9.0/Microsoft.Agents.AI.dll + lib/net9.0/Microsoft.Agents.AI.dll + true + CP0002 M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory @@ -85,6 +141,20 @@ lib/net9.0/Microsoft.Agents.AI.dll true + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[]) + lib/netstandard2.0/Microsoft.Agents.AI.dll + lib/netstandard2.0/Microsoft.Agents.AI.dll + true + + + CP0002 + M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory) + lib/netstandard2.0/Microsoft.Agents.AI.dll + lib/netstandard2.0/Microsoft.Agents.AI.dll + true + CP0002 M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory