Skip to content

Commit e5f7b9c

Browse files
.NET: Support reflection for discovery of resources and scripts in class-based skills (#5183)
* support reflection for discovery of resources and scripts in class-based skills * fix format issues * refactor samples to use reflection * Validate resource member signatures during discovery Add discovery-time validation in AgentClassSkill.DiscoverResources() to fail fast when [AgentSkillResource] is applied to members with incompatible signatures: - Reject indexer properties (getter has parameters) - Reject methods with parameters other than IServiceProvider or CancellationToken Throws InvalidOperationException with actionable error messages instead of allowing silent runtime failures when ReadAsync invokes the AIFunction with no named arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * prevent duplicates --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4a36f10 commit e5f7b9c

19 files changed

Lines changed: 1508 additions & 299 deletions

dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<Nullable>enable</Nullable>
88
<ImplicitUsings>enable</ImplicitUsings>
9-
<NoWarn>$(NoWarn);MAAI001</NoWarn>
9+
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
1010
</PropertyGroup>
1111

1212
<ItemGroup>

dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
// This sample demonstrates how to define Agent Skills as C# classes using AgentClassSkill.
4-
// Class-based skills bundle all components into a single class implementation.
3+
// This sample demonstrates how to define Agent Skills as C# classes using AgentClassSkill
4+
// with attributes for automatic script and resource discovery.
55

6+
using System.ComponentModel;
67
using System.Text.Json;
78
using Azure.AI.OpenAI;
89
using Azure.Identity;
@@ -44,17 +45,16 @@
4445
Console.WriteLine($"Agent: {response.Text}");
4546

4647
/// <summary>
47-
/// A unit-converter skill defined as a C# class.
48+
/// A unit-converter skill defined as a C# class using attributes for discovery.
4849
/// </summary>
4950
/// <remarks>
50-
/// Class-based skills bundle all components (name, description, body, resources, scripts)
51-
/// into a single class.
51+
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
52+
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
53+
/// are automatically discovered as skill scripts. Alternatively,
54+
/// <see cref="AgentSkill.Resources"/> and <see cref="AgentSkill.Scripts"/> can be overridden.
5255
/// </remarks>
53-
internal sealed class UnitConverterSkill : AgentClassSkill
56+
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
5457
{
55-
private IReadOnlyList<AgentSkillResource>? _resources;
56-
private IReadOnlyList<AgentSkillScript>? _scripts;
57-
5858
/// <inheritdoc/>
5959
public override AgentSkillFrontmatter Frontmatter { get; } = new(
6060
"unit-converter",
@@ -69,31 +69,40 @@ Use this skill when the user asks to convert between units.
6969
3. Present the result clearly with both units.
7070
""";
7171

72-
/// <inheritdoc/>
73-
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
74-
[
75-
CreateResource(
76-
"conversion-table",
77-
"""
78-
# Conversion Tables
79-
80-
Formula: **result = value × factor**
81-
82-
| From | To | Factor |
83-
|-------------|-------------|----------|
84-
| miles | kilometers | 1.60934 |
85-
| kilometers | miles | 0.621371 |
86-
| pounds | kilograms | 0.453592 |
87-
| kilograms | pounds | 2.20462 |
88-
"""),
89-
];
90-
91-
/// <inheritdoc/>
92-
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
93-
[
94-
CreateScript("convert", ConvertUnits),
95-
];
72+
/// <summary>
73+
/// Gets the <see cref="JsonSerializerOptions"/> used to marshal parameters and return values
74+
/// for scripts and resources.
75+
/// </summary>
76+
/// <remarks>
77+
/// This override is not necessary for this sample, but can be used to provide custom
78+
/// serialization options, for example a source-generated <c>JsonTypeInfoResolver</c>
79+
/// for Native AOT compatibility.
80+
/// </remarks>
81+
protected override JsonSerializerOptions? SerializerOptions => null;
82+
83+
/// <summary>
84+
/// A conversion table resource providing multiplication factors.
85+
/// </summary>
86+
[AgentSkillResource("conversion-table")]
87+
[Description("Lookup table of multiplication factors for common unit conversions.")]
88+
public string ConversionTable => """
89+
# Conversion Tables
90+
91+
Formula: **result = value × factor**
92+
93+
| From | To | Factor |
94+
|-------------|-------------|----------|
95+
| miles | kilometers | 1.60934 |
96+
| kilometers | miles | 0.621371 |
97+
| pounds | kilograms | 0.453592 |
98+
| kilograms | pounds | 2.20462 |
99+
""";
96100

101+
/// <summary>
102+
/// Converts a value by the given factor.
103+
/// </summary>
104+
[AgentSkillScript("convert")]
105+
[Description("Multiplies a value by a conversion factor and returns the result as JSON.")]
97106
private static string ConvertUnits(double value, double factor)
98107
{
99108
double result = Math.Round(value * factor, 4);

dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
# Class-Based Agent Skills Sample
22

3-
This sample demonstrates how to define **Agent Skills as C# classes** using `AgentClassSkill`.
3+
This sample demonstrates how to define **Agent Skills as C# classes** using `AgentClassSkill`
4+
with **attributes** for automatic script and resource discovery.
45

56
## What it demonstrates
67

78
- Creating skills as classes that extend `AgentClassSkill`
8-
- Bundling name, description, body, resources, and scripts into a single class
9+
- Using `[AgentSkillResource]` on properties to define resources
10+
- Using `[AgentSkillScript]` on methods to define scripts
11+
- Automatic discovery (no need to override `Resources`/`Scripts`)
912
- Using the `AgentSkillsProvider` constructor with class-based skills
13+
- Overriding `SerializerOptions` for Native AOT compatibility
1014

1115
## Skills Included
1216

dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<Nullable>enable</Nullable>
88
<ImplicitUsings>enable</ImplicitUsings>
9-
<NoWarn>$(NoWarn);MAAI001</NoWarn>
9+
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
1010
</PropertyGroup>
1111

1212
<ItemGroup>

dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
// Three different skill sources are registered here:
99
// 1. File-based: unit-converter (miles↔km, pounds↔kg) from SKILL.md on disk
1010
// 2. Code-defined: volume-converter (gallons↔liters) using AgentInlineSkill
11-
// 3. Class-based: temperature-converter (°F↔°C↔K) using AgentClassSkill
11+
// 3. Class-based: temperature-converter (°F↔°C↔K) using AgentClassSkill with attributes
1212
//
1313
// For simpler, single-source scenarios, see the earlier steps in this sample series
1414
// (e.g., Step01 for file-based, Step02 for code-defined, Step03 for class-based).
1515

16+
using System.ComponentModel;
1617
using System.Text.Json;
1718
using Azure.AI.OpenAI;
1819
using Azure.Identity;
@@ -89,13 +90,15 @@ 1. Review the volume-conversion-table resource to find the correct factor.
8990
Console.WriteLine($"Agent: {response.Text}");
9091

9192
/// <summary>
92-
/// A temperature-converter skill defined as a C# class.
93+
/// A temperature-converter skill defined as a C# class using attributes for discovery.
9394
/// </summary>
94-
internal sealed class TemperatureConverterSkill : AgentClassSkill
95+
/// <remarks>
96+
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
97+
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
98+
/// are automatically discovered as skill scripts.
99+
/// </remarks>
100+
internal sealed class TemperatureConverterSkill : AgentClassSkill<TemperatureConverterSkill>
95101
{
96-
private IReadOnlyList<AgentSkillResource>? _resources;
97-
private IReadOnlyList<AgentSkillScript>? _scripts;
98-
99102
/// <inheritdoc/>
100103
public override AgentSkillFrontmatter Frontmatter { get; } = new(
101104
"temperature-converter",
@@ -110,29 +113,27 @@ Use this skill when the user asks to convert temperatures.
110113
3. Present the result clearly with both temperature scales.
111114
""";
112115

113-
/// <inheritdoc/>
114-
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
115-
[
116-
CreateResource(
117-
"temperature-conversion-formulas",
118-
"""
119-
# Temperature Conversion Formulas
120-
121-
| From | To | Formula |
122-
|-------------|-------------|---------------------------|
123-
| Fahrenheit | Celsius | °C = (°F − 32) × 5/9 |
124-
| Celsius | Fahrenheit | °F = (°C × 9/5) + 32 |
125-
| Celsius | Kelvin | K = °C + 273.15 |
126-
| Kelvin | Celsius | °C = K − 273.15 |
127-
"""),
128-
];
129-
130-
/// <inheritdoc/>
131-
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
132-
[
133-
CreateScript("convert-temperature", ConvertTemperature),
134-
];
116+
/// <summary>
117+
/// A reference table of temperature conversion formulas.
118+
/// </summary>
119+
[AgentSkillResource("temperature-conversion-formulas")]
120+
[Description("Formulas for converting between Fahrenheit, Celsius, and Kelvin.")]
121+
public string ConversionFormulas => """
122+
# Temperature Conversion Formulas
123+
124+
| From | To | Formula |
125+
|-------------|-------------|---------------------------|
126+
| Fahrenheit | Celsius | °C = (°F − 32) × 5/9 |
127+
| Celsius | Fahrenheit | °F = (°C × 9/5) + 32 |
128+
| Celsius | Kelvin | K = °C + 273.15 |
129+
| Kelvin | Celsius | °C = K − 273.15 |
130+
""";
135131

132+
/// <summary>
133+
/// Converts a temperature value between scales.
134+
/// </summary>
135+
[AgentSkillScript("convert-temperature")]
136+
[Description("Converts a temperature value from one scale to another.")]
136137
private static string ConvertTemperature(double value, string from, string to)
137138
{
138139
double result = (from.ToUpperInvariant(), to.ToUpperInvariant()) switch

dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<Nullable>enable</Nullable>
88
<ImplicitUsings>enable</ImplicitUsings>
9-
<NoWarn>$(NoWarn);MAAI001;CA1812</NoWarn>
9+
<NoWarn>$(NoWarn);MAAI001;CA1812;IDE0051</NoWarn>
1010
</PropertyGroup>
1111

1212
<ItemGroup>

dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// showing that DI works identically regardless of how the skill is defined.
1414
// When prompted with a question spanning both domains, the agent uses both skills.
1515

16+
using System.ComponentModel;
1617
using System.Text.Json;
1718
using Azure.AI.OpenAI;
1819
using Azure.Identity;
@@ -62,8 +63,8 @@ Use this skill when the user asks to convert between distance units (miles and k
6263
// Approach 2: Class-Based Skill with DI (AgentClassSkill)
6364
// =====================================================================
6465
// Handles weight conversions (pounds ↔ kilograms).
65-
// Resources and scripts are encapsulated in a class. Factory methods
66-
// CreateResource and CreateScript accept delegates with IServiceProvider.
66+
// Resources and scripts are discovered via reflection using attributes.
67+
// Methods with an IServiceProvider parameter receive DI automatically.
6768
//
6869
// Alternatively, class-based skills can accept dependencies through their
6970
// constructor. Register the skill class itself in the ServiceCollection and
@@ -113,14 +114,13 @@ Use this skill when the user asks to convert between distance units (miles and k
113114
/// </summary>
114115
/// <remarks>
115116
/// This skill resolves <see cref="ConversionService"/> from the DI container
116-
/// in both its resource and script functions. This enables clean separation of
117-
/// concerns and testability while retaining the class-based skill pattern.
117+
/// in both its resource and script methods. Methods with an <see cref="IServiceProvider"/>
118+
/// parameter are automatically injected by the framework. Properties and methods annotated
119+
/// with <see cref="AgentSkillResourceAttribute"/> and <see cref="AgentSkillScriptAttribute"/>
120+
/// are automatically discovered via reflection.
118121
/// </remarks>
119-
internal sealed class WeightConverterSkill : AgentClassSkill
122+
internal sealed class WeightConverterSkill : AgentClassSkill<WeightConverterSkill>
120123
{
121-
private IReadOnlyList<AgentSkillResource>? _resources;
122-
private IReadOnlyList<AgentSkillScript>? _scripts;
123-
124124
/// <inheritdoc/>
125125
public override AgentSkillFrontmatter Frontmatter { get; } = new(
126126
"weight-converter",
@@ -135,25 +135,27 @@ Use this skill when the user asks to convert between weight units (pounds and ki
135135
3. Present the result clearly with both units.
136136
""";
137137

138-
/// <inheritdoc/>
139-
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
140-
[
141-
CreateResource("weight-table", (IServiceProvider serviceProvider) =>
142-
{
143-
var service = serviceProvider.GetRequiredService<ConversionService>();
144-
return service.GetWeightTable();
145-
}),
146-
];
138+
/// <summary>
139+
/// Returns the weight conversion table from the DI-registered <see cref="ConversionService"/>.
140+
/// </summary>
141+
[AgentSkillResource("weight-table")]
142+
[Description("Lookup table of multiplication factors for weight conversions.")]
143+
private static string GetWeightTable(IServiceProvider serviceProvider)
144+
{
145+
var service = serviceProvider.GetRequiredService<ConversionService>();
146+
return service.GetWeightTable();
147+
}
147148

148-
/// <inheritdoc/>
149-
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
150-
[
151-
CreateScript("convert", (double value, double factor, IServiceProvider serviceProvider) =>
152-
{
153-
var service = serviceProvider.GetRequiredService<ConversionService>();
154-
return service.Convert(value, factor);
155-
}),
156-
];
149+
/// <summary>
150+
/// Converts a value by the given factor using the DI-registered <see cref="ConversionService"/>.
151+
/// </summary>
152+
[AgentSkillScript("convert")]
153+
[Description("Multiplies a value by a conversion factor and returns the result as JSON.")]
154+
private static string Convert(double value, double factor, IServiceProvider serviceProvider)
155+
{
156+
var service = serviceProvider.GetRequiredService<ConversionService>();
157+
return service.Convert(value, factor);
158+
}
157159
}
158160

159161
// ---------------------------------------------------------------------------

dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace Microsoft.Agents.AI;
2121
/// </para>
2222
/// <list type="bullet">
2323
/// <item><description><strong>Mixed skill types</strong> — combine file-based, code-defined (<see cref="AgentInlineSkill"/>),
24-
/// and class-based (<see cref="AgentClassSkill"/>) skills in a single provider.</description></item>
24+
/// and class-based (<see cref="AgentClassSkill{TSelf}"/>) skills in a single provider.</description></item>
2525
/// <item><description><strong>Multiple file script runners</strong> — use different script runners for different
2626
/// file skill directories via per-source <c>scriptRunner</c> parameters on
2727
/// <see cref="UseFileSkill"/> / <see cref="UseFileSkills(IEnumerable{string}, AgentFileSkillsSourceOptions?, AgentFileSkillScriptRunner?)"/>.</description></item>

0 commit comments

Comments
 (0)