mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e864cdb4c | ||
|
|
14d2ab3262 | ||
|
|
e5f7b9c260 |
@@ -1,21 +1,21 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<RCNumber>6</RCNumber>
|
||||
<VersionPrefix>1.1.0</VersionPrefix>
|
||||
<RCNumber>1</RCNumber>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260402.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260402.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260410.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260410.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||
<GitTag>1.0.0</GitTag>
|
||||
<GitTag>1.1.0</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
<!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
|
||||
<PackageValidationBaselineVersion>1.0.0-rc5</PackageValidationBaselineVersion>
|
||||
<!-- Enable validation for RC packages and GA packages -->
|
||||
<EnablePackageValidation Condition="'$(IsReleaseCandidate)' == 'true' OR '$(IsReleased)' == 'true'">true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
|
||||
<!-- Enable validation for GA packages -->
|
||||
<EnablePackageValidation Condition="'$(IsReleased)' == 'true'">true</EnablePackageValidation>
|
||||
<!-- Validate assembly attributes only for Publish builds -->
|
||||
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
|
||||
<!-- Do not validate reference assemblies -->
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// 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.
|
||||
// This sample demonstrates how to define Agent Skills as C# classes using AgentClassSkill
|
||||
// with attributes for automatic script and resource discovery.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -44,17 +45,16 @@ AgentResponse response = await agent.RunAsync(
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
|
||||
/// <summary>
|
||||
/// A unit-converter skill defined as a C# class.
|
||||
/// A unit-converter skill defined as a C# class using attributes for discovery.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Class-based skills bundle all components (name, description, body, resources, scripts)
|
||||
/// into a single class.
|
||||
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
|
||||
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
|
||||
/// are automatically discovered as skill scripts. Alternatively,
|
||||
/// <see cref="AgentSkill.Resources"/> and <see cref="AgentSkill.Scripts"/> can be overridden.
|
||||
/// </remarks>
|
||||
internal sealed class UnitConverterSkill : AgentClassSkill
|
||||
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new(
|
||||
"unit-converter",
|
||||
@@ -69,31 +69,40 @@ internal sealed class UnitConverterSkill : AgentClassSkill
|
||||
3. Present the result clearly with both units.
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
CreateResource(
|
||||
"conversion-table",
|
||||
"""
|
||||
# Conversion Tables
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonSerializerOptions"/> used to marshal parameters and return values
|
||||
/// for scripts and resources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This override is not necessary for this sample, but can be used to provide custom
|
||||
/// serialization options, for example a source-generated <c>JsonTypeInfoResolver</c>
|
||||
/// for Native AOT compatibility.
|
||||
/// </remarks>
|
||||
protected override JsonSerializerOptions? SerializerOptions => null;
|
||||
|
||||
Formula: **result = value × factor**
|
||||
/// <summary>
|
||||
/// A conversion table resource providing multiplication factors.
|
||||
/// </summary>
|
||||
[AgentSkillResource("conversion-table")]
|
||||
[Description("Lookup table of multiplication factors for common unit conversions.")]
|
||||
public string ConversionTable => """
|
||||
# Conversion Tables
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
"""),
|
||||
];
|
||||
Formula: **result = value × factor**
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
CreateScript("convert", ConvertUnits),
|
||||
];
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// Converts a value by the given factor.
|
||||
/// </summary>
|
||||
[AgentSkillScript("convert")]
|
||||
[Description("Multiplies a value by a conversion factor and returns the result as JSON.")]
|
||||
private static string ConvertUnits(double value, double factor)
|
||||
{
|
||||
double result = Math.Round(value * factor, 4);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# Class-Based Agent Skills Sample
|
||||
|
||||
This sample demonstrates how to define **Agent Skills as C# classes** using `AgentClassSkill`.
|
||||
This sample demonstrates how to define **Agent Skills as C# classes** using `AgentClassSkill`
|
||||
with **attributes** for automatic script and resource discovery.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- Creating skills as classes that extend `AgentClassSkill`
|
||||
- Bundling name, description, body, resources, and scripts into a single class
|
||||
- Using `[AgentSkillResource]` on properties to define resources
|
||||
- Using `[AgentSkillScript]` on methods to define scripts
|
||||
- Automatic discovery (no need to override `Resources`/`Scripts`)
|
||||
- Using the `AgentSkillsProvider` constructor with class-based skills
|
||||
- Overriding `SerializerOptions` for Native AOT compatibility
|
||||
|
||||
## Skills Included
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
// 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
|
||||
// 3. Class-based: temperature-converter (°F↔°C↔K) using AgentClassSkill with attributes
|
||||
//
|
||||
// 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.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -89,13 +90,15 @@ AgentResponse response = await agent.RunAsync(
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
|
||||
/// <summary>
|
||||
/// A temperature-converter skill defined as a C# class.
|
||||
/// A temperature-converter skill defined as a C# class using attributes for discovery.
|
||||
/// </summary>
|
||||
internal sealed class TemperatureConverterSkill : AgentClassSkill
|
||||
/// <remarks>
|
||||
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
|
||||
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
|
||||
/// are automatically discovered as skill scripts.
|
||||
/// </remarks>
|
||||
internal sealed class TemperatureConverterSkill : AgentClassSkill<TemperatureConverterSkill>
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new(
|
||||
"temperature-converter",
|
||||
@@ -110,29 +113,27 @@ internal sealed class TemperatureConverterSkill : AgentClassSkill
|
||||
3. Present the result clearly with both temperature scales.
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
CreateResource(
|
||||
"temperature-conversion-formulas",
|
||||
"""
|
||||
# Temperature Conversion Formulas
|
||||
/// <summary>
|
||||
/// A reference table of temperature conversion formulas.
|
||||
/// </summary>
|
||||
[AgentSkillResource("temperature-conversion-formulas")]
|
||||
[Description("Formulas for converting between Fahrenheit, Celsius, and Kelvin.")]
|
||||
public string ConversionFormulas => """
|
||||
# Temperature Conversion Formulas
|
||||
|
||||
| From | To | Formula |
|
||||
|-------------|-------------|---------------------------|
|
||||
| Fahrenheit | Celsius | °C = (°F − 32) × 5/9 |
|
||||
| Celsius | Fahrenheit | °F = (°C × 9/5) + 32 |
|
||||
| Celsius | Kelvin | K = °C + 273.15 |
|
||||
| Kelvin | Celsius | °C = K − 273.15 |
|
||||
"""),
|
||||
];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
CreateScript("convert-temperature", ConvertTemperature),
|
||||
];
|
||||
| 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 |
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// Converts a temperature value between scales.
|
||||
/// </summary>
|
||||
[AgentSkillScript("convert-temperature")]
|
||||
[Description("Converts a temperature value from one scale to another.")]
|
||||
private static string ConvertTemperature(double value, string from, string to)
|
||||
{
|
||||
double result = (from.ToUpperInvariant(), to.ToUpperInvariant()) switch
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001;CA1812</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001;CA1812;IDE0051</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -62,8 +63,8 @@ var distanceSkill = new AgentInlineSkill(
|
||||
// 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.
|
||||
// Resources and scripts are discovered via reflection using attributes.
|
||||
// Methods with an IServiceProvider parameter receive DI automatically.
|
||||
//
|
||||
// Alternatively, class-based skills can accept dependencies through their
|
||||
// constructor. Register the skill class itself in the ServiceCollection and
|
||||
@@ -113,14 +114,13 @@ Console.WriteLine($"Agent: {response.Text}");
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This skill resolves <see cref="ConversionService"/> from the DI container
|
||||
/// in both its resource and script functions. This enables clean separation of
|
||||
/// concerns and testability while retaining the class-based skill pattern.
|
||||
/// in both its resource and script methods. Methods with an <see cref="IServiceProvider"/>
|
||||
/// parameter are automatically injected by the framework. Properties and methods annotated
|
||||
/// with <see cref="AgentSkillResourceAttribute"/> and <see cref="AgentSkillScriptAttribute"/>
|
||||
/// are automatically discovered via reflection.
|
||||
/// </remarks>
|
||||
internal sealed class WeightConverterSkill : AgentClassSkill
|
||||
internal sealed class WeightConverterSkill : AgentClassSkill<WeightConverterSkill>
|
||||
{
|
||||
private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } = new(
|
||||
"weight-converter",
|
||||
@@ -135,25 +135,27 @@ internal sealed class WeightConverterSkill : AgentClassSkill
|
||||
3. Present the result clearly with both units.
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
|
||||
[
|
||||
CreateResource("weight-table", (IServiceProvider serviceProvider) =>
|
||||
{
|
||||
var service = serviceProvider.GetRequiredService<ConversionService>();
|
||||
return service.GetWeightTable();
|
||||
}),
|
||||
];
|
||||
/// <summary>
|
||||
/// Returns the weight conversion table from the DI-registered <see cref="ConversionService"/>.
|
||||
/// </summary>
|
||||
[AgentSkillResource("weight-table")]
|
||||
[Description("Lookup table of multiplication factors for weight conversions.")]
|
||||
private static string GetWeightTable(IServiceProvider serviceProvider)
|
||||
{
|
||||
var service = serviceProvider.GetRequiredService<ConversionService>();
|
||||
return service.GetWeightTable();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
|
||||
[
|
||||
CreateScript("convert", (double value, double factor, IServiceProvider serviceProvider) =>
|
||||
{
|
||||
var service = serviceProvider.GetRequiredService<ConversionService>();
|
||||
return service.Convert(value, factor);
|
||||
}),
|
||||
];
|
||||
/// <summary>
|
||||
/// Converts a value by the given factor using the DI-registered <see cref="ConversionService"/>.
|
||||
/// </summary>
|
||||
[AgentSkillScript("convert")]
|
||||
[Description("Multiplies a value by a conversion factor and returns the result as JSON.")]
|
||||
private static string Convert(double value, double factor, IServiceProvider serviceProvider)
|
||||
{
|
||||
var service = serviceProvider.GetRequiredService<ConversionService>();
|
||||
return service.Convert(value, factor);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:OpenAI.Assistants.OpenAIAssistantClientExtensions</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.OpenAI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.OpenAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:OpenAI.Assistants.OpenAIAssistantClientExtensions</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.OpenAI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.OpenAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:OpenAI.Assistants.OpenAIAssistantClientExtensions</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.OpenAI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.OpenAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:OpenAI.Assistants.OpenAIAssistantClientExtensions</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.OpenAI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.OpenAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0001</DiagnosticId>
|
||||
<Target>T:OpenAI.Assistants.OpenAIAssistantClientExtensions</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.OpenAI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.OpenAI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.Declarative.Foundry.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -43,27 +43,6 @@
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.set_SimulateServiceStoredChatHistory(System.Boolean)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Extensions.AI.ChatClientBuilderExtensions.UseServiceStoredChatHistorySimulation(Microsoft.Extensions.AI.ChatClientBuilder)</Target>
|
||||
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -106,27 +85,6 @@
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.set_SimulateServiceStoredChatHistory(System.Boolean)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Extensions.AI.ChatClientBuilderExtensions.UseServiceStoredChatHistorySimulation(Microsoft.Extensions.AI.ChatClientBuilder)</Target>
|
||||
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -169,27 +127,6 @@
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.set_SimulateServiceStoredChatHistory(System.Boolean)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Extensions.AI.ChatClientBuilderExtensions.UseServiceStoredChatHistorySimulation(Microsoft.Extensions.AI.ChatClientBuilder)</Target>
|
||||
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -232,27 +169,6 @@
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.set_SimulateServiceStoredChatHistory(System.Boolean)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Extensions.AI.ChatClientBuilderExtensions.UseServiceStoredChatHistorySimulation(Microsoft.Extensions.AI.ChatClientBuilder)</Target>
|
||||
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
|
||||
@@ -295,25 +211,4 @@
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.set_SimulateServiceStoredChatHistory(System.Boolean)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Microsoft.Extensions.AI.ChatClientBuilderExtensions.UseServiceStoredChatHistorySimulation(Microsoft.Extensions.AI.ChatClientBuilder)</Target>
|
||||
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
|
||||
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -21,7 +21,7 @@ namespace Microsoft.Agents.AI;
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><strong>Mixed skill types</strong> — combine file-based, code-defined (<see cref="AgentInlineSkill"/>),
|
||||
/// and class-based (<see cref="AgentClassSkill"/>) skills in a single provider.</description></item>
|
||||
/// and class-based (<see cref="AgentClassSkill{TSelf}"/>) skills in a single provider.</description></item>
|
||||
/// <item><description><strong>Multiple file script runners</strong> — use different script runners for different
|
||||
/// file skill directories via per-source <c>scriptRunner</c> parameters on
|
||||
/// <see cref="UseFileSkill"/> / <see cref="UseFileSkills(IEnumerable{string}, AgentFileSkillsSourceOptions?, AgentFileSkillScriptRunner?)"/>.</description></item>
|
||||
|
||||
@@ -32,15 +32,15 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
private const string SkillFileName = "SKILL.md";
|
||||
private const int MaxSearchDepth = 2;
|
||||
|
||||
// "." means the skill directory root itself (no sub-folder descent constraint)
|
||||
private const string RootFolderIndicator = ".";
|
||||
// "." means the skill directory root itself (no subdirectory descent constraint)
|
||||
private const string RootDirectoryIndicator = ".";
|
||||
|
||||
private static readonly string[] s_defaultScriptExtensions = [".py", ".js", ".sh", ".ps1", ".cs", ".csx"];
|
||||
private static readonly string[] s_defaultResourceExtensions = [".md", ".json", ".yaml", ".yml", ".csv", ".xml", ".txt"];
|
||||
|
||||
// Standard sub-folder names per https://agentskills.io/specification#directory-structure
|
||||
private static readonly string[] s_defaultScriptFolders = ["scripts"];
|
||||
private static readonly string[] s_defaultResourceFolders = ["references", "assets"];
|
||||
// Standard subdirectory names per https://agentskills.io/specification#directory-structure
|
||||
private static readonly string[] s_defaultScriptDirectories = ["scripts"];
|
||||
private static readonly string[] s_defaultResourceDirectories = ["references", "assets"];
|
||||
|
||||
// Matches YAML frontmatter delimited by "---" lines. Group 1 = content between delimiters.
|
||||
// Multiline makes ^/$ match line boundaries; Singleline makes . match newlines across the block.
|
||||
@@ -63,8 +63,8 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
private readonly IEnumerable<string> _skillPaths;
|
||||
private readonly HashSet<string> _allowedResourceExtensions;
|
||||
private readonly HashSet<string> _allowedScriptExtensions;
|
||||
private readonly IReadOnlyList<string> _scriptFolders;
|
||||
private readonly IReadOnlyList<string> _resourceFolders;
|
||||
private readonly IReadOnlyList<string> _scriptDirectories;
|
||||
private readonly IReadOnlyList<string> _resourceDirectories;
|
||||
private readonly AgentFileSkillScriptRunner? _scriptRunner;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
@@ -111,13 +111,13 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
options?.AllowedScriptExtensions ?? s_defaultScriptExtensions,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
this._scriptFolders = options?.ScriptFolders is not null
|
||||
? [.. ValidateAndNormalizeFolderNames(options.ScriptFolders, this._logger)]
|
||||
: s_defaultScriptFolders;
|
||||
this._scriptDirectories = options?.ScriptDirectories is not null
|
||||
? [.. ValidateAndNormalizeDirectoryNames(options.ScriptDirectories, this._logger)]
|
||||
: s_defaultScriptDirectories;
|
||||
|
||||
this._resourceFolders = options?.ResourceFolders is not null
|
||||
? [.. ValidateAndNormalizeFolderNames(options.ResourceFolders, this._logger)]
|
||||
: s_defaultResourceFolders;
|
||||
this._resourceDirectories = options?.ResourceDirectories is not null
|
||||
? [.. ValidateAndNormalizeDirectoryNames(options.ResourceDirectories, this._logger)]
|
||||
: s_defaultResourceDirectories;
|
||||
|
||||
this._scriptRunner = scriptRunner;
|
||||
}
|
||||
@@ -303,12 +303,12 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans configured resource folders within a skill directory for resource files matching the configured extensions.
|
||||
/// Scans configured resource directories within a skill directory for resource files matching the configured extensions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default, scans <c>references/</c> and <c>assets/</c> sub-folders as specified by the
|
||||
/// By default, scans <c>references/</c> and <c>assets/</c> subdirectories as specified by the
|
||||
/// <see href="https://agentskills.io/specification">Agent Skills specification</see>.
|
||||
/// Configure <see cref="AgentFileSkillsSourceOptions.ResourceFolders"/> to scan different or
|
||||
/// Configure <see cref="AgentFileSkillsSourceOptions.ResourceDirectories"/> to scan different or
|
||||
/// additional directories, including <c>"."</c> for the skill root itself.
|
||||
/// Each file is validated against path-traversal and symlink-escape checks; unsafe files are skipped.
|
||||
/// </remarks>
|
||||
@@ -316,14 +316,14 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
{
|
||||
var resources = new List<AgentFileSkillResource>();
|
||||
|
||||
foreach (string folder in this._resourceFolders.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
foreach (string directory in this._resourceDirectories.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
bool isRootFolder = string.Equals(folder, RootFolderIndicator, StringComparison.Ordinal);
|
||||
bool isRootDirectory = string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal);
|
||||
|
||||
// GetFullPath normalizes mixed separators (e.g. "C:\skill\scripts/f1" → "C:\skill\scripts\f1")
|
||||
string targetDirectory = isRootFolder
|
||||
string targetDirectory = isRootDirectory
|
||||
? skillDirectoryFullPath
|
||||
: Path.GetFullPath(Path.Combine(skillDirectoryFullPath, folder)) + Path.DirectorySeparatorChar;
|
||||
: Path.GetFullPath(Path.Combine(skillDirectoryFullPath, directory)) + Path.DirectorySeparatorChar;
|
||||
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
@@ -331,13 +331,13 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
}
|
||||
|
||||
// Directory-level symlink check: skip if targetDirectory (or any intermediate
|
||||
// segment) is a reparse point. The root folder is excluded — it's a caller-supplied
|
||||
// segment) is a reparse point. The root directory is excluded — it's a caller-supplied
|
||||
// trusted path, and the security boundary guards files within it, not the path itself.
|
||||
if (!isRootFolder && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath))
|
||||
if (!isRootDirectory && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath))
|
||||
{
|
||||
if (this._logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
LogResourceSymlinkFolder(this._logger, skillName, SanitizePathForLog(folder));
|
||||
LogResourceSymlinkDirectory(this._logger, skillName, SanitizePathForLog(directory));
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -380,7 +380,7 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
// e.g. "references/../../../etc/shadow" → "/etc/shadow"
|
||||
string resolvedFilePath = Path.GetFullPath(filePath);
|
||||
|
||||
// Path containment: reject if the resolved path escapes the target folder.
|
||||
// Path containment: reject if the resolved path escapes the target directory.
|
||||
// e.g. "/etc/shadow".StartsWith("/skills/myskill/references/") → false → skip
|
||||
if (!resolvedFilePath.StartsWith(targetDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -416,12 +416,12 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans configured script folders within a skill directory for script files matching the configured extensions.
|
||||
/// Scans configured script directories within a skill directory for script files matching the configured extensions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default, scans the <c>scripts/</c> sub-folder as specified by the
|
||||
/// By default, scans the <c>scripts/</c> subdirectory as specified by the
|
||||
/// <see href="https://agentskills.io/specification">Agent Skills specification</see>.
|
||||
/// Configure <see cref="AgentFileSkillsSourceOptions.ScriptFolders"/> to scan different or
|
||||
/// Configure <see cref="AgentFileSkillsSourceOptions.ScriptDirectories"/> to scan different or
|
||||
/// additional directories, including <c>"."</c> for the skill root itself.
|
||||
/// Each file is validated against path-traversal and symlink-escape checks; unsafe files are skipped.
|
||||
/// </remarks>
|
||||
@@ -429,14 +429,14 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
{
|
||||
var scripts = new List<AgentFileSkillScript>();
|
||||
|
||||
foreach (string folder in this._scriptFolders.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
foreach (string directory in this._scriptDirectories.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
bool isRootFolder = string.Equals(folder, RootFolderIndicator, StringComparison.Ordinal);
|
||||
bool isRootDirectory = string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal);
|
||||
|
||||
// GetFullPath normalizes mixed separators (e.g. "C:\skill\scripts/f1" → "C:\skill\scripts\f1")
|
||||
string targetDirectory = isRootFolder
|
||||
string targetDirectory = isRootDirectory
|
||||
? skillDirectoryFullPath
|
||||
: Path.GetFullPath(Path.Combine(skillDirectoryFullPath, folder)) + Path.DirectorySeparatorChar;
|
||||
: Path.GetFullPath(Path.Combine(skillDirectoryFullPath, directory)) + Path.DirectorySeparatorChar;
|
||||
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
@@ -444,13 +444,13 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
}
|
||||
|
||||
// Directory-level symlink check: skip if targetDirectory (or any intermediate
|
||||
// segment) is a reparse point. The root folder is excluded — it's a caller-supplied
|
||||
// segment) is a reparse point. The root directory is excluded — it's a caller-supplied
|
||||
// trusted path, and the security boundary guards files within it, not the path itself.
|
||||
if (!isRootFolder && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath))
|
||||
if (!isRootDirectory && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath))
|
||||
{
|
||||
if (this._logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
LogScriptSymlinkFolder(this._logger, skillName, SanitizePathForLog(folder));
|
||||
LogScriptSymlinkDirectory(this._logger, skillName, SanitizePathForLog(directory));
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -480,7 +480,7 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
// e.g. "scripts/../../../etc/shadow" → "/etc/shadow"
|
||||
string resolvedFilePath = Path.GetFullPath(filePath);
|
||||
|
||||
// Path containment: reject if the resolved path escapes the target folder.
|
||||
// Path containment: reject if the resolved path escapes the target directory.
|
||||
// e.g. "/etc/shadow".StartsWith("/skills/myskill/scripts/") → false → skip
|
||||
if (!resolvedFilePath.StartsWith(targetDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -541,8 +541,8 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a relative path or folder name by stripping a leading "./"/".\",
|
||||
/// trimming trailing directory separators, and replacing backslashes with forward
|
||||
/// Normalizes a relative path or directory name by stripping a leading "./"/".\",
|
||||
/// trimming trailing separators, and replacing backslashes with forward
|
||||
/// slashes.
|
||||
/// </summary>
|
||||
private static string NormalizePath(string path)
|
||||
@@ -602,36 +602,36 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ValidateAndNormalizeFolderNames(IEnumerable<string> folders, ILogger logger)
|
||||
private static IEnumerable<string> ValidateAndNormalizeDirectoryNames(IEnumerable<string> directories, ILogger logger)
|
||||
{
|
||||
foreach (string folder in folders)
|
||||
foreach (string directory in directories)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(folder))
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
throw new ArgumentException("Folder names must not be null or whitespace.", nameof(folders));
|
||||
throw new ArgumentException("Directory names must not be null or whitespace.", nameof(directories));
|
||||
}
|
||||
|
||||
// "." is valid — it means the skill root directory.
|
||||
if (string.Equals(folder, RootFolderIndicator, StringComparison.Ordinal))
|
||||
if (string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal))
|
||||
{
|
||||
yield return folder;
|
||||
yield return directory;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reject absolute paths and any path segments that escape upward.
|
||||
if (Path.IsPathRooted(folder) || ContainsParentTraversalSegment(folder))
|
||||
if (Path.IsPathRooted(directory) || ContainsParentTraversalSegment(directory))
|
||||
{
|
||||
LogFolderNameSkippedInvalid(logger, folder);
|
||||
LogDirectoryNameSkippedInvalid(logger, directory);
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return NormalizePath(folder);
|
||||
yield return NormalizePath(directory);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsParentTraversalSegment(string folder)
|
||||
private static bool ContainsParentTraversalSegment(string directory)
|
||||
{
|
||||
foreach (string segment in folder.Split('/', '\\'))
|
||||
foreach (string segment in directory.Split('/', '\\'))
|
||||
{
|
||||
if (segment == "..")
|
||||
{
|
||||
@@ -666,8 +666,8 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
[LoggerMessage(LogLevel.Warning, "Skipping resource in skill '{SkillName}': '{ResourcePath}' is a symlink that resolves outside the skill directory")]
|
||||
private static partial void LogResourceSymlinkEscape(ILogger logger, string skillName, string resourcePath);
|
||||
|
||||
[LoggerMessage(LogLevel.Warning, "Skipping resource folder '{FolderName}' in skill '{SkillName}': folder path contains a symlink")]
|
||||
private static partial void LogResourceSymlinkFolder(ILogger logger, string skillName, string folderName);
|
||||
[LoggerMessage(LogLevel.Warning, "Skipping resource directory '{DirectoryName}' in skill '{SkillName}': directory path contains a symlink")]
|
||||
private static partial void LogResourceSymlinkDirectory(ILogger logger, string skillName, string directoryName);
|
||||
|
||||
[LoggerMessage(LogLevel.Debug, "Skipping file '{FilePath}' in skill '{SkillName}': extension '{Extension}' is not in the allowed list")]
|
||||
private static partial void LogResourceSkippedExtension(ILogger logger, string skillName, string filePath, string extension);
|
||||
@@ -678,9 +678,9 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
[LoggerMessage(LogLevel.Warning, "Skipping script in skill '{SkillName}': '{ScriptPath}' is a symlink that resolves outside the skill directory")]
|
||||
private static partial void LogScriptSymlinkEscape(ILogger logger, string skillName, string scriptPath);
|
||||
|
||||
[LoggerMessage(LogLevel.Warning, "Skipping script folder '{FolderName}' in skill '{SkillName}': folder path contains a symlink")]
|
||||
private static partial void LogScriptSymlinkFolder(ILogger logger, string skillName, string folderName);
|
||||
[LoggerMessage(LogLevel.Warning, "Skipping script directory '{DirectoryName}' in skill '{SkillName}': directory path contains a symlink")]
|
||||
private static partial void LogScriptSymlinkDirectory(ILogger logger, string skillName, string directoryName);
|
||||
|
||||
[LoggerMessage(LogLevel.Warning, "Skipping invalid folder name '{FolderName}': must be a relative path with no '..' segments")]
|
||||
private static partial void LogFolderNameSkippedInvalid(ILogger logger, string folderName);
|
||||
[LoggerMessage(LogLevel.Warning, "Skipping invalid directory name '{DirectoryName}': must be a relative path with no '..' segments")]
|
||||
private static partial void LogDirectoryNameSkippedInvalid(ILogger logger, string directoryName);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class AgentFileSkillsSourceOptions
|
||||
public IEnumerable<string>? AllowedScriptExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets relative folder paths to scan for script files within each skill directory.
|
||||
/// Gets or sets relative directory paths to scan for script files within each skill directory.
|
||||
/// Values may be single-segment names (e.g., <c>"scripts"</c>) or multi-segment relative
|
||||
/// paths (e.g., <c>"sub/scripts"</c>). Use <c>"."</c> to include files directly at the
|
||||
/// skill root. Leading <c>"./"</c> prefixes, trailing separators, and backslashes are
|
||||
@@ -42,10 +42,10 @@ public sealed class AgentFileSkillsSourceOptions
|
||||
/// <see href="https://agentskills.io/specification">Agent Skills specification</see>).
|
||||
/// When set, replaces the defaults entirely.
|
||||
/// </summary>
|
||||
public IEnumerable<string>? ScriptFolders { get; set; }
|
||||
public IEnumerable<string>? ScriptDirectories { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets relative folder paths to scan for resource files within each skill directory.
|
||||
/// Gets or sets relative directory paths to scan for resource files within each skill directory.
|
||||
/// Values may be single-segment names (e.g., <c>"references"</c>) or multi-segment relative
|
||||
/// paths (e.g., <c>"sub/resources"</c>). Use <c>"."</c> to include files directly at the
|
||||
/// skill root. Leading <c>"./"</c> prefixes, trailing separators, and backslashes are
|
||||
@@ -55,5 +55,5 @@ public sealed class AgentFileSkillsSourceOptions
|
||||
/// <see href="https://agentskills.io/specification">Agent Skills specification</see>).
|
||||
/// When set, replaces the defaults entirely.
|
||||
/// </summary>
|
||||
public IEnumerable<string>? ResourceFolders { get; set; }
|
||||
public IEnumerable<string>? ResourceDirectories { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
@@ -11,17 +15,55 @@ namespace Microsoft.Agents.AI;
|
||||
/// <summary>
|
||||
/// Abstract base class for defining skills as C# classes that bundle all components together.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelf">
|
||||
/// The concrete skill type. This type parameter is annotated with
|
||||
/// <see cref="DynamicallyAccessedMembersAttribute"/> to ensure that the IL trimmer and Native AOT compiler
|
||||
/// preserve the members needed for attribute-based discovery.
|
||||
/// </typeparam>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Inherit from this class to create a self-contained skill definition. Override the abstract
|
||||
/// properties to provide name, description, and instructions. Use <see cref="CreateResource(string, object, string?)"/>,
|
||||
/// <see cref="CreateResource(string, Delegate, string?, JsonSerializerOptions?)"/>, and <see cref="CreateScript"/> to define
|
||||
/// inline resources and scripts.
|
||||
/// properties to provide name, description, and instructions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Scripts and resources can be defined in two ways:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <b>Attribute-based (recommended):</b> Annotate methods with <see cref="AgentSkillScriptAttribute"/> to define scripts,
|
||||
/// and properties or methods with <see cref="AgentSkillResourceAttribute"/> to define resources. These are automatically
|
||||
/// discovered via reflection on <typeparamref name="TSelf"/>. This approach is compatible with Native AOT.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <b>Explicit override:</b> Override <see cref="AgentSkill.Resources"/> and <see cref="AgentSkill.Scripts"/>, using
|
||||
/// <see cref="CreateResource(string, object, string?)"/>, <see cref="CreateResource(string, Delegate, string?, JsonSerializerOptions?)"/>,
|
||||
/// and <see cref="CreateScript"/> to define inline resources and scripts. This approach is also compatible with Native AOT.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Multi-level inheritance limitation:</b> Discovery reflects only on <typeparamref name="TSelf"/>,
|
||||
/// so if a further-derived subclass adds new attributed members, they will not be discovered unless
|
||||
/// that subclass also uses the CRTP pattern
|
||||
/// (e.g., <c>class SpecialSkill : AgentClassSkill<SpecialSkill></c>).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public class PdfFormatterSkill : AgentClassSkill
|
||||
/// // Attribute-based approach (recommended, AOT-compatible):
|
||||
/// public class PdfFormatterSkill : AgentClassSkill<PdfFormatterSkill>
|
||||
/// {
|
||||
/// public override AgentSkillFrontmatter Frontmatter { get; } = new("pdf-formatter", "Format documents as PDF.");
|
||||
/// protected override string Instructions => "Use this skill to format documents...";
|
||||
///
|
||||
/// [AgentSkillResource("template")]
|
||||
/// public string Template => "Use this template...";
|
||||
///
|
||||
/// [AgentSkillScript("format-pdf")]
|
||||
/// private static string FormatPdf(string content) => content;
|
||||
/// }
|
||||
///
|
||||
/// // Explicit override approach (AOT-compatible):
|
||||
/// public class ExplicitPdfFormatterSkill : AgentClassSkill<ExplicitPdfFormatterSkill>
|
||||
/// {
|
||||
/// private IReadOnlyList<AgentSkillResource>? _resources;
|
||||
/// private IReadOnlyList<AgentSkillScript>? _scripts;
|
||||
@@ -44,15 +86,41 @@ namespace Microsoft.Agents.AI;
|
||||
/// </code>
|
||||
/// </example>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public abstract class AgentClassSkill : AgentSkill
|
||||
public abstract class AgentClassSkill<
|
||||
[DynamicallyAccessedMembers(
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods)] TSelf>
|
||||
: AgentSkill
|
||||
where TSelf : AgentClassSkill<TSelf>
|
||||
{
|
||||
private const BindingFlags DiscoveryBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
|
||||
private string? _content;
|
||||
private bool _resourcesDiscovered;
|
||||
private bool _scriptsDiscovered;
|
||||
private IReadOnlyList<AgentSkillResource>? _reflectedResources;
|
||||
private IReadOnlyList<AgentSkillScript>? _reflectedScripts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw instructions text for this skill.
|
||||
/// </summary>
|
||||
protected abstract string Instructions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonSerializerOptions"/> used to marshal parameters and return values
|
||||
/// for scripts and resources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override this property to provide custom serialization options. This value is used by
|
||||
/// reflection-discovered scripts and resources, and also as a fallback by <see cref="CreateScript"/>
|
||||
/// and <see cref="CreateResource(string, Delegate, string?, JsonSerializerOptions?)"/> when no
|
||||
/// explicit <see cref="JsonSerializerOptions"/> is passed to those methods.
|
||||
/// The default value is <see langword="null"/>, which causes <see cref="AIJsonUtilities.DefaultOptions"/> to be used.
|
||||
/// </remarks>
|
||||
protected virtual JsonSerializerOptions? SerializerOptions => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Returns a synthesized XML document containing name, description, instructions, resources, and scripts.
|
||||
@@ -65,6 +133,48 @@ public abstract class AgentClassSkill : AgentSkill
|
||||
this.Resources,
|
||||
this.Scripts);
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Returns resources discovered via reflection by scanning <typeparamref name="TSelf"/> for
|
||||
/// members annotated with <see cref="AgentSkillResourceAttribute"/>. This discovery is
|
||||
/// compatible with Native AOT because <typeparamref name="TSelf"/> is annotated with
|
||||
/// <see cref="DynamicallyAccessedMembersAttribute"/>. The result is cached after the first access.
|
||||
/// </remarks>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!this._resourcesDiscovered)
|
||||
{
|
||||
this._reflectedResources = this.DiscoverResources();
|
||||
this._resourcesDiscovered = true;
|
||||
}
|
||||
|
||||
return this._reflectedResources;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Returns scripts discovered via reflection by scanning <typeparamref name="TSelf"/> for
|
||||
/// methods annotated with <see cref="AgentSkillScriptAttribute"/>. This discovery is
|
||||
/// compatible with Native AOT because <typeparamref name="TSelf"/> is annotated with
|
||||
/// <see cref="DynamicallyAccessedMembersAttribute"/>. The result is cached after the first access.
|
||||
/// </remarks>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!this._scriptsDiscovered)
|
||||
{
|
||||
this._reflectedScripts = this.DiscoverScripts();
|
||||
this._scriptsDiscovered = true;
|
||||
}
|
||||
|
||||
return this._reflectedScripts;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a skill resource backed by a static value.
|
||||
/// </summary>
|
||||
@@ -72,7 +182,7 @@ public abstract class AgentClassSkill : AgentSkill
|
||||
/// <param name="value">The static resource value.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
/// <returns>A new <see cref="AgentSkillResource"/> instance.</returns>
|
||||
protected static AgentSkillResource CreateResource(string name, object value, string? description = null)
|
||||
protected AgentSkillResource CreateResource(string name, object value, string? description = null)
|
||||
=> new AgentInlineSkillResource(name, value, description);
|
||||
|
||||
/// <summary>
|
||||
@@ -83,11 +193,11 @@ public abstract class AgentClassSkill : AgentSkill
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
/// <param name="serializerOptions">
|
||||
/// Optional <see cref="JsonSerializerOptions"/> used to marshal the delegate's parameters and return value.
|
||||
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
|
||||
/// When <see langword="null"/>, falls back to <see cref="SerializerOptions"/>.
|
||||
/// </param>
|
||||
/// <returns>A new <see cref="AgentSkillResource"/> instance.</returns>
|
||||
protected static AgentSkillResource CreateResource(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
|
||||
=> new AgentInlineSkillResource(name, method, description, serializerOptions);
|
||||
protected AgentSkillResource CreateResource(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
|
||||
=> new AgentInlineSkillResource(name, method, description, serializerOptions ?? this.SerializerOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a skill script backed by a delegate.
|
||||
@@ -97,9 +207,129 @@ public abstract class AgentClassSkill : AgentSkill
|
||||
/// <param name="description">An optional description of the script.</param>
|
||||
/// <param name="serializerOptions">
|
||||
/// Optional <see cref="JsonSerializerOptions"/> used to marshal the delegate's parameters and return value.
|
||||
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
|
||||
/// When <see langword="null"/>, falls back to <see cref="SerializerOptions"/>.
|
||||
/// </param>
|
||||
/// <returns>A new <see cref="AgentSkillScript"/> instance.</returns>
|
||||
protected static AgentSkillScript CreateScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
|
||||
=> new AgentInlineSkillScript(name, method, description, serializerOptions);
|
||||
protected AgentSkillScript CreateScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
|
||||
=> new AgentInlineSkillScript(name, method, description, serializerOptions ?? this.SerializerOptions);
|
||||
|
||||
private List<AgentSkillResource>? DiscoverResources()
|
||||
{
|
||||
List<AgentSkillResource>? resources = null;
|
||||
|
||||
var selfType = typeof(TSelf);
|
||||
|
||||
// Discover resources from properties annotated with [AgentSkillResource].
|
||||
foreach (var property in selfType.GetProperties(DiscoveryBindingFlags))
|
||||
{
|
||||
var attr = property.GetCustomAttribute<AgentSkillResourceAttribute>();
|
||||
if (attr is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var getter = property.GetGetMethod(nonPublic: true);
|
||||
if (getter is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Indexer properties have getter parameters and cannot be used as resources
|
||||
// because ReadAsync invokes the underlying AIFunction with no named arguments.
|
||||
if (getter.GetParameters().Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Property '{property.Name}' on type '{selfType.Name}' is an indexer and cannot be used as a skill resource. " +
|
||||
"Remove the [AgentSkillResource] attribute or use a non-indexer property.");
|
||||
}
|
||||
|
||||
var name = attr.Name ?? property.Name;
|
||||
if (resources?.Exists(r => r.Name == name) == true)
|
||||
{
|
||||
throw new InvalidOperationException($"Skill '{this.Frontmatter.Name}' already has a resource named '{name}'. Ensure each [AgentSkillResource] has a unique name.");
|
||||
}
|
||||
|
||||
resources ??= [];
|
||||
resources.Add(new AgentInlineSkillResource(
|
||||
name: name,
|
||||
method: getter,
|
||||
target: getter.IsStatic ? null : this,
|
||||
description: property.GetCustomAttribute<DescriptionAttribute>()?.Description,
|
||||
serializerOptions: this.SerializerOptions));
|
||||
}
|
||||
|
||||
// Discover resources from methods annotated with [AgentSkillResource].
|
||||
foreach (var method in selfType.GetMethods(DiscoveryBindingFlags))
|
||||
{
|
||||
var attr = method.GetCustomAttribute<AgentSkillResourceAttribute>();
|
||||
if (attr is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ValidateResourceMethodParameters(method, selfType);
|
||||
|
||||
var name = attr.Name ?? method.Name;
|
||||
if (resources?.Exists(r => r.Name == name) == true)
|
||||
{
|
||||
throw new InvalidOperationException($"Skill '{this.Frontmatter.Name}' already has a resource named '{name}'. Ensure each [AgentSkillResource] has a unique name.");
|
||||
}
|
||||
|
||||
resources ??= [];
|
||||
resources.Add(new AgentInlineSkillResource(
|
||||
name: name,
|
||||
method: method,
|
||||
target: method.IsStatic ? null : this,
|
||||
description: method.GetCustomAttribute<DescriptionAttribute>()?.Description,
|
||||
serializerOptions: this.SerializerOptions));
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
private static void ValidateResourceMethodParameters(MethodInfo method, Type skillType)
|
||||
{
|
||||
foreach (var param in method.GetParameters())
|
||||
{
|
||||
if (param.ParameterType != typeof(IServiceProvider) &&
|
||||
param.ParameterType != typeof(CancellationToken))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method '{method.Name}' on type '{skillType.Name}' has parameter '{param.Name}' of type " +
|
||||
$"'{param.ParameterType}' which cannot be supplied when reading a resource. " +
|
||||
"Resource methods may only accept IServiceProvider and/or CancellationToken parameters. " +
|
||||
"Remove the [AgentSkillResource] attribute or change the method signature.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<AgentSkillScript>? DiscoverScripts()
|
||||
{
|
||||
List<AgentSkillScript>? scripts = null;
|
||||
|
||||
foreach (var method in typeof(TSelf).GetMethods(DiscoveryBindingFlags))
|
||||
{
|
||||
var attr = method.GetCustomAttribute<AgentSkillScriptAttribute>();
|
||||
if (attr is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = attr.Name ?? method.Name;
|
||||
if (scripts?.Exists(s => s.Name == name) == true)
|
||||
{
|
||||
throw new InvalidOperationException($"Skill '{this.Frontmatter.Name}' already has a script named '{name}'. Ensure each [AgentSkillScript] has a unique name.");
|
||||
}
|
||||
|
||||
scripts ??= [];
|
||||
scripts.Add(new AgentInlineSkillScript(
|
||||
name: name,
|
||||
method: method,
|
||||
target: method.IsStatic ? null : this,
|
||||
description: method.GetCustomAttribute<DescriptionAttribute>()?.Description,
|
||||
serializerOptions: this.SerializerOptions));
|
||||
}
|
||||
|
||||
return scripts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -54,6 +55,28 @@ internal sealed class AgentInlineSkillResource : AgentSkillResource
|
||||
this._function = AIFunctionFactory.Create(method, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentInlineSkillResource"/> class from a <see cref="MethodInfo"/>.
|
||||
/// The method is invoked via an <see cref="AIFunction"/> each time <see cref="ReadAsync"/> is called,
|
||||
/// producing a dynamic (computed) value.
|
||||
/// </summary>
|
||||
/// <param name="name">The resource name.</param>
|
||||
/// <param name="method">A method that produces the resource value when requested.</param>
|
||||
/// <param name="target">The target instance for instance methods, or <see langword="null"/> for static methods.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
/// <param name="serializerOptions">
|
||||
/// Optional <see cref="JsonSerializerOptions"/> used to marshal the method's parameters and return value.
|
||||
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
|
||||
/// </param>
|
||||
public AgentInlineSkillResource(string name, MethodInfo method, object? target, string? description = null, JsonSerializerOptions? serializerOptions = null)
|
||||
: base(name, description)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
var options = new AIFunctionFactoryOptions { Name = this.Name, SerializerOptions = serializerOptions };
|
||||
this._function = AIFunctionFactory.Create(method, target, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -39,6 +40,27 @@ internal sealed class AgentInlineSkillScript : AgentSkillScript
|
||||
this._function = AIFunctionFactory.Create(method, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentInlineSkillScript"/> class from a <see cref="MethodInfo"/>.
|
||||
/// The method's parameters and return type are automatically marshaled via <see cref="AIFunctionFactory"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The script name.</param>
|
||||
/// <param name="method">The method to execute when the script is invoked.</param>
|
||||
/// <param name="target">The target instance for instance methods, or <see langword="null"/> for static methods.</param>
|
||||
/// <param name="description">An optional description of the script.</param>
|
||||
/// <param name="serializerOptions">
|
||||
/// Optional <see cref="JsonSerializerOptions"/> used to marshal the method's parameters and return value.
|
||||
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
|
||||
/// </param>
|
||||
public AgentInlineSkillScript(string name, MethodInfo method, object? target, string? description = null, JsonSerializerOptions? serializerOptions = null)
|
||||
: base(Throw.IfNullOrWhitespace(name), description)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
var options = new AIFunctionFactoryOptions { Name = this.Name, SerializerOptions = serializerOptions };
|
||||
this._function = AIFunctionFactory.Create(method, target, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JSON schema describing the parameters accepted by this script, or <see langword="null"/> if not available.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a property or method as a skill resource that is automatically discovered by <see cref="AgentClassSkill{TSelf}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Apply this attribute to properties or methods in an <see cref="AgentClassSkill{TSelf}"/> subclass to register
|
||||
/// them as skill resources.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To provide a description for the resource, apply <see cref="DescriptionAttribute"/>
|
||||
/// to the same member.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When applied to a <b>property</b>, the property getter is invoked each time the resource is read,
|
||||
/// enabling dynamic (computed) resources. When applied to a <b>method</b>, the method is invoked each time
|
||||
/// the resource is read, also enabling dynamic resources. Methods with an
|
||||
/// <see cref="IServiceProvider"/> parameter support dependency injection.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This attribute is compatible with Native AOT when used with <see cref="AgentClassSkill{TSelf}"/>.
|
||||
/// Alternatively, override the <see cref="AgentSkill.Resources"/> property and use
|
||||
/// <see cref="AgentClassSkill{TSelf}.CreateResource(string, object, string?)"/> instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public class MySkill : AgentClassSkill<MySkill>
|
||||
/// {
|
||||
/// public override AgentSkillFrontmatter Frontmatter { get; } = new("my-skill", "A skill.");
|
||||
/// protected override string Instructions => "Use this skill to do something.";
|
||||
///
|
||||
/// [AgentSkillResource("reference-data")]
|
||||
/// [Description("Some reference content for the skill.")]
|
||||
/// public string ReferenceData => "Some reference content.";
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class AgentSkillResourceAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSkillResourceAttribute"/> class.
|
||||
/// The resource name defaults to the property or method name.
|
||||
/// </summary>
|
||||
public AgentSkillResourceAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSkillResourceAttribute"/> class
|
||||
/// with an explicit resource name.
|
||||
/// </summary>
|
||||
/// <param name="name">The resource name used to identify this resource.</param>
|
||||
public AgentSkillResourceAttribute(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resource name, or <see langword="null"/> to use the member name.
|
||||
/// </summary>
|
||||
public string? Name { get; }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a method as a skill script that is automatically discovered by <see cref="AgentClassSkill{TSelf}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Apply this attribute to methods in an <see cref="AgentClassSkill{TSelf}"/> subclass to register them as
|
||||
/// skill scripts. The method's parameters and return type are automatically marshaled via
|
||||
/// <c>AIFunctionFactory</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To provide a description for the script, apply <see cref="DescriptionAttribute"/>
|
||||
/// to the same method.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Methods can be instance or static, and may have any visibility (public, private, etc.).
|
||||
/// Methods with an <see cref="IServiceProvider"/> parameter support dependency injection.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This attribute is compatible with Native AOT when used with <see cref="AgentClassSkill{TSelf}"/>.
|
||||
/// Alternatively, override the <see cref="AgentSkill.Scripts"/> property and use
|
||||
/// <see cref="AgentClassSkill{TSelf}.CreateScript"/> instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public class MySkill : AgentClassSkill<MySkill>
|
||||
/// {
|
||||
/// public override AgentSkillFrontmatter Frontmatter { get; } = new("my-skill", "A skill.");
|
||||
/// protected override string Instructions => "Use this skill to do something.";
|
||||
///
|
||||
/// [AgentSkillScript("do-something")]
|
||||
/// [Description("Converts the input to upper case.")]
|
||||
/// private static string DoSomething(string input) => input.ToUpperInvariant();
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class AgentSkillScriptAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSkillScriptAttribute"/> class.
|
||||
/// The script name defaults to the method name.
|
||||
/// </summary>
|
||||
public AgentSkillScriptAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSkillScriptAttribute"/> class
|
||||
/// with an explicit script name.
|
||||
/// </summary>
|
||||
/// <param name="name">The script name used to identify this script.</param>
|
||||
public AgentSkillScriptAttribute(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the script name, or <see langword="null"/> to use the method name.
|
||||
/// </summary>
|
||||
public string? Name { get; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+18
-18
@@ -116,8 +116,8 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_ScriptsOutsideScriptsDir_AreNotDiscoveredAsync()
|
||||
{
|
||||
// Arrange — scripts outside configured folders are not discovered; only files directly
|
||||
// inside the configured folder are picked up (no subdirectory recursion)
|
||||
// Arrange — scripts outside configured directories are not discovered; only files directly
|
||||
// inside the configured directory are picked up (no subdirectory recursion)
|
||||
string skillDir = CreateSkillDir(this._testRoot, "root-scripts", "Root scripts skill", "Body.");
|
||||
CreateFile(skillDir, "convert.py", "print('root')");
|
||||
CreateFile(skillDir, "tools/helper.sh", "echo 'helper'");
|
||||
@@ -126,7 +126,7 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync(CancellationToken.None);
|
||||
|
||||
// Assert — neither file is in the default scripts/ folder, so no scripts are discovered
|
||||
// Assert — neither file is in the default scripts/ directory, so no scripts are discovered
|
||||
Assert.Single(skills);
|
||||
Assert.Empty(skills[0].Scripts!);
|
||||
}
|
||||
@@ -229,18 +229,18 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_ScriptFoldersWithNestedPath_DiscoversScriptsAsync()
|
||||
public async Task GetSkillsAsync_ScriptDirectoriesWithNestedPath_DiscoversScriptsAsync()
|
||||
{
|
||||
// Arrange — ScriptFolders configured with a multi-segment relative path (f1/f2/f3)
|
||||
string skillDir = CreateSkillDir(this._testRoot, "nested-script-skill", "Nested script folder", "Body.");
|
||||
// Arrange — ScriptDirectories configured with a multi-segment relative path (f1/f2/f3)
|
||||
string skillDir = CreateSkillDir(this._testRoot, "nested-script-skill", "Nested script directory", "Body.");
|
||||
CreateFile(skillDir, "f1/f2/f3/run.py", "print('nested')");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ScriptFolders = ["f1/f2/f3"] });
|
||||
new AgentFileSkillsSourceOptions { ScriptDirectories = ["f1/f2/f3"] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync(CancellationToken.None);
|
||||
|
||||
// Assert — script file inside the deeply nested folder is discovered
|
||||
// Assert — script file inside the deeply nested directory is discovered
|
||||
Assert.Single(skills);
|
||||
Assert.Single(skills[0].Scripts!);
|
||||
Assert.Equal("f1/f2/f3/run.py", skills[0].Scripts![0].Name);
|
||||
@@ -250,29 +250,29 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable
|
||||
[InlineData("./scripts")]
|
||||
[InlineData("./scripts/f1")]
|
||||
[InlineData("./scripts/f1", "./f2")]
|
||||
public async Task GetSkillsAsync_ScriptFolderWithDotSlashPrefix_DiscoversScriptsAsync(params string[] folders)
|
||||
public async Task GetSkillsAsync_ScriptDirectoryWithDotSlashPrefix_DiscoversScriptsAsync(params string[] directories)
|
||||
{
|
||||
// Arrange — "./"-prefixed folders are equivalent to their counterparts without the prefix;
|
||||
// Arrange — "./"-prefixed directories are equivalent to their counterparts without the prefix;
|
||||
// the leading "./" is transparently normalized by Path.GetFullPath during file enumeration.
|
||||
string skillDir = CreateSkillDir(this._testRoot, "dotslash-script-skill", "Dot-slash prefix", "Body.");
|
||||
foreach (string folder in folders)
|
||||
foreach (string directory in directories)
|
||||
{
|
||||
string folderWithoutDotSlash = folder.Substring(2); // strip "./"
|
||||
CreateFile(skillDir, $"{folderWithoutDotSlash}/run.py", "print('dotslash')");
|
||||
string directoryWithoutDotSlash = directory.Substring(2); // strip "./"
|
||||
CreateFile(skillDir, $"{directoryWithoutDotSlash}/run.py", "print('dotslash')");
|
||||
}
|
||||
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ScriptFolders = folders });
|
||||
new AgentFileSkillsSourceOptions { ScriptDirectories = directories });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync(CancellationToken.None);
|
||||
|
||||
// Assert — scripts are discovered with names identical to using folders without "./"
|
||||
// Assert — scripts are discovered with names identical to using directories without "./"
|
||||
Assert.Single(skills);
|
||||
Assert.Equal(folders.Length, skills[0].Scripts!.Count);
|
||||
foreach (string folder in folders)
|
||||
Assert.Equal(directories.Length, skills[0].Scripts!.Count);
|
||||
foreach (string directory in directories)
|
||||
{
|
||||
string expectedName = $"{folder.Substring(2)}/run.py";
|
||||
string expectedName = $"{directory.Substring(2)}/run.py";
|
||||
Assert.Contains(skills[0].Scripts!, s => s.Name == expectedName);
|
||||
}
|
||||
}
|
||||
|
||||
+55
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -167,4 +168,58 @@ public sealed class AgentInlineSkillResourceTests
|
||||
// Assert
|
||||
Assert.Equal("value", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MethodInfo_SetsNameAndDescription()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(AgentInlineSkillResourceTests).GetMethod(nameof(StaticResourceHelper), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
// Act
|
||||
var resource = new AgentInlineSkillResource("method-resource", method, target: null, description: "A method resource.");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("method-resource", resource.Name);
|
||||
Assert.Equal("A method resource.", resource.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_MethodInfo_StaticMethod_ReturnsValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(AgentInlineSkillResourceTests).GetMethod(nameof(StaticResourceHelper), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
var resource = new AgentInlineSkillResource("static-method-res", method, target: null);
|
||||
|
||||
// Act
|
||||
var result = await resource.ReadAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("static-resource-value", result?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_MethodInfo_InstanceMethod_ReturnsValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(AgentInlineSkillResourceTests).GetMethod(nameof(InstanceResourceHelper), BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var resource = new AgentInlineSkillResource("instance-method-res", method, target: this);
|
||||
|
||||
// Act
|
||||
var result = await resource.ReadAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("instance-resource-value", result?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MethodInfo_NullMethod_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new AgentInlineSkillResource("my-res", null!, target: null));
|
||||
}
|
||||
|
||||
private static string StaticResourceHelper() => "static-resource-value";
|
||||
|
||||
private string InstanceResourceHelper() => "instance-resource-value";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -152,4 +153,77 @@ public sealed class AgentInlineSkillScriptTests
|
||||
// Assert
|
||||
Assert.Equal("hello world", result?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MethodInfo_SetsNameAndDescription()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(AgentInlineSkillScriptTests).GetMethod(nameof(StaticScriptHelper), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
// Act
|
||||
var script = new AgentInlineSkillScript("method-script", method, target: null, description: "A method script.");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("method-script", script.Name);
|
||||
Assert.Equal("A method script.", script.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_MethodInfo_StaticMethod_InvokesAndReturnsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(AgentInlineSkillScriptTests).GetMethod(nameof(StaticScriptHelper), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
var script = new AgentInlineSkillScript("static-method-script", method, target: null);
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
var args = new AIFunctionArguments { ["input"] = "hello" };
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("HELLO", result?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_MethodInfo_InstanceMethod_InvokesAndReturnsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(AgentInlineSkillScriptTests).GetMethod(nameof(InstanceScriptHelper), BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var script = new AgentInlineSkillScript("instance-method-script", method, target: this);
|
||||
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
|
||||
var args = new AIFunctionArguments { ["input"] = "test" };
|
||||
|
||||
// Act
|
||||
var result = await script.RunAsync(skill, args, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test-suffix", result?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MethodInfo_NullMethod_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new AgentInlineSkillScript("my-script", null!, target: null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParametersSchema_MethodInfo_ContainsParameterNames()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(AgentInlineSkillScriptTests).GetMethod(nameof(StaticScriptHelper), BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
var script = new AgentInlineSkillScript("param-script", method, target: null);
|
||||
|
||||
// Act
|
||||
var schema = script.ParametersSchema;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(schema);
|
||||
Assert.Contains("input", schema!.Value.GetRawText());
|
||||
}
|
||||
|
||||
private static string StaticScriptHelper(string input) => input.ToUpperInvariant();
|
||||
|
||||
private string InstanceScriptHelper(string input) => input + "-suffix";
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AgentSkillResourceAttribute"/>.
|
||||
/// </summary>
|
||||
public sealed class AgentSkillResourceAttributeTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultConstructor_NameIsNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
var attr = new AgentSkillResourceAttribute();
|
||||
|
||||
// Assert
|
||||
Assert.Null(attr.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NamedConstructor_SetsName()
|
||||
{
|
||||
// Arrange & Act
|
||||
var attr = new AgentSkillResourceAttribute("my-resource");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("my-resource", attr.Name);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AgentSkillScriptAttribute"/>.
|
||||
/// </summary>
|
||||
public sealed class AgentSkillScriptAttributeTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultConstructor_NameIsNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
var attr = new AgentSkillScriptAttribute();
|
||||
|
||||
// Assert
|
||||
Assert.Null(attr.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NamedConstructor_SetsName()
|
||||
{
|
||||
// Arrange & Act
|
||||
var attr = new AgentSkillScriptAttribute("my-script");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("my-script", attr.Name);
|
||||
}
|
||||
}
|
||||
@@ -871,7 +871,7 @@ public sealed class AgentSkillsProviderTests : IDisposable
|
||||
public async Task Constructor_ClassSkillsEnumerable_ProvidesSkillsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skills = new List<AgentClassSkill>
|
||||
var skills = new List<AgentSkill>
|
||||
{
|
||||
new TestClassSkill("enum-class-a", "Class A", "Instructions A."),
|
||||
new TestClassSkill("enum-class-b", "Class B", "Instructions B."),
|
||||
@@ -928,7 +928,7 @@ public sealed class AgentSkillsProviderTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestClassSkill : AgentClassSkill
|
||||
private sealed class TestClassSkill : AgentClassSkill<TestClassSkill>
|
||||
{
|
||||
private readonly string _instructions;
|
||||
|
||||
|
||||
+65
-65
@@ -199,7 +199,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_FilesWithMatchingExtensions_DiscoveredAsResourcesAsync()
|
||||
{
|
||||
// Arrange — create resource files in spec-defined sub-folders
|
||||
// Arrange — create resource files in spec-defined subdirectories
|
||||
string skillDir = Path.Combine(this._testRoot, "resource-skill");
|
||||
string refsDir = Path.Combine(skillDir, "references");
|
||||
string assetsDir = Path.Combine(skillDir, "assets");
|
||||
@@ -226,7 +226,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_FilesWithNonMatchingExtensions_NotDiscoveredAsync()
|
||||
{
|
||||
// Arrange — create a file with an extension not in the default list inside a spec folder
|
||||
// Arrange — create a file with an extension not in the default list inside a spec directory
|
||||
string skillDir = Path.Combine(this._testRoot, "ext-skill");
|
||||
string refsDir = Path.Combine(skillDir, "references");
|
||||
Directory.CreateDirectory(refsDir);
|
||||
@@ -300,7 +300,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_CustomResourceExtensions_UsedForDiscoveryAsync()
|
||||
{
|
||||
// Arrange — use a source with custom extensions; files placed in spec folder
|
||||
// Arrange — use a source with custom extensions; files placed in spec directory
|
||||
string skillDir = Path.Combine(this._testRoot, "custom-ext-skill");
|
||||
string refsDir = Path.Combine(skillDir, "references");
|
||||
Directory.CreateDirectory(refsDir);
|
||||
@@ -364,7 +364,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_ResourceInSkillRoot_NotDiscoveredByDefaultAsync()
|
||||
{
|
||||
// Arrange — resource files directly in the skill directory (not in a spec sub-folder)
|
||||
// Arrange — resource files directly in the skill directory (not in a spec subdirectory)
|
||||
string skillDir = Path.Combine(this._testRoot, "root-resource-skill");
|
||||
Directory.CreateDirectory(skillDir);
|
||||
File.WriteAllText(Path.Combine(skillDir, "guide.md"), "guide content");
|
||||
@@ -377,15 +377,15 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
|
||||
// Assert — root-level files are NOT discovered unless "." is in ResourceFolders
|
||||
// Assert — root-level files are NOT discovered unless "." is in ResourceDirectories
|
||||
Assert.Single(skills);
|
||||
Assert.Empty(skills[0].Resources!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_ResourceInSkillRoot_DiscoveredWhenRootFolderConfiguredAsync()
|
||||
public async Task GetSkillsAsync_ResourceInSkillRoot_DiscoveredWhenRootDirectoryConfiguredAsync()
|
||||
{
|
||||
// Arrange — "." in ResourceFolders opts into root-level resource discovery
|
||||
// Arrange — "." in ResourceDirectories opts into root-level resource discovery
|
||||
string skillDir = Path.Combine(this._testRoot, "root-opt-in-skill");
|
||||
Directory.CreateDirectory(skillDir);
|
||||
File.WriteAllText(Path.Combine(skillDir, "guide.md"), "guide content");
|
||||
@@ -394,7 +394,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: root-opt-in-skill\ndescription: Root opt-in\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ResourceFolders = ["references", "assets", "."] });
|
||||
new AgentFileSkillsSourceOptions { ResourceDirectories = ["references", "assets", "."] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
@@ -408,31 +408,31 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_ResourceInNonSpecFolder_NotDiscoveredByDefaultAsync()
|
||||
public async Task GetSkillsAsync_ResourceInNonSpecDirectory_NotDiscoveredByDefaultAsync()
|
||||
{
|
||||
// Arrange — resource in a non-spec folder (neither references/ nor assets/)
|
||||
// Arrange — resource in a non-spec directory (neither references/ nor assets/)
|
||||
string skillDir = Path.Combine(this._testRoot, "non-spec-skill");
|
||||
string customDir = Path.Combine(skillDir, "docs");
|
||||
Directory.CreateDirectory(customDir);
|
||||
File.WriteAllText(Path.Combine(customDir, "readme.md"), "docs content");
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: non-spec-skill\ndescription: Non-spec folder\n---\nBody.");
|
||||
"---\nname: non-spec-skill\ndescription: Non-spec directory\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor);
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
|
||||
// Assert — non-spec folders are not scanned by default
|
||||
// Assert — non-spec directories are not scanned by default
|
||||
Assert.Single(skills);
|
||||
Assert.Empty(skills[0].Resources!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_CustomResourceFolders_ReplacesDefaultsAsync()
|
||||
public async Task GetSkillsAsync_CustomResourceDirectories_ReplacesDefaultsAsync()
|
||||
{
|
||||
// Arrange — custom ResourceFolders replaces the spec defaults
|
||||
string skillDir = Path.Combine(this._testRoot, "custom-folder-skill");
|
||||
// Arrange — custom ResourceDirectories replaces the spec defaults
|
||||
string skillDir = Path.Combine(this._testRoot, "custom-directory-skill");
|
||||
string customDir = Path.Combine(skillDir, "docs");
|
||||
string refsDir = Path.Combine(skillDir, "references");
|
||||
Directory.CreateDirectory(customDir);
|
||||
@@ -441,9 +441,9 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
File.WriteAllText(Path.Combine(refsDir, "ref.md"), "ref content");
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: custom-folder-skill\ndescription: Custom folder\n---\nBody.");
|
||||
"---\nname: custom-directory-skill\ndescription: Custom directory\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ResourceFolders = ["docs"] });
|
||||
new AgentFileSkillsSourceOptions { ResourceDirectories = ["docs"] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
@@ -518,7 +518,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
[Fact]
|
||||
public async Task ReadSkillResourceAsync_ValidResource_ReturnsContentAsync()
|
||||
{
|
||||
// Arrange — create a skill with a resource file discovered from the references folder
|
||||
// Arrange — create a skill with a resource file discovered from the references directory
|
||||
string skillDir = this.CreateSkillDirectory("read-skill", "A skill", "See docs for details.");
|
||||
string refsDir = Path.Combine(skillDir, "references");
|
||||
Directory.CreateDirectory(refsDir);
|
||||
@@ -614,12 +614,12 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_SymlinkedResourceFolder_SkipsWithoutEnumeratingAsync()
|
||||
public async Task GetSkillsAsync_SymlinkedResourceDirectory_SkipsWithoutEnumeratingAsync()
|
||||
{
|
||||
// Arrange — references/ is a symlink pointing outside the skill directory.
|
||||
// The directory-level check should skip it entirely (no file enumeration),
|
||||
// so even files with valid extensions in the target are not discovered.
|
||||
string skillDir = Path.Combine(this._testRoot, "symlink-folder-skip");
|
||||
string skillDir = Path.Combine(this._testRoot, "symlink-directory-skip");
|
||||
string assetsDir = Path.Combine(skillDir, "assets");
|
||||
Directory.CreateDirectory(assetsDir);
|
||||
File.WriteAllText(Path.Combine(assetsDir, "legit.md"), "legit content");
|
||||
@@ -642,21 +642,21 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: symlink-folder-skip\ndescription: Symlinked folder skip\n---\nBody.");
|
||||
"---\nname: symlink-directory-skip\ndescription: Symlinked directory skip\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor);
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
|
||||
// Assert — only assets/legit.md is found; the symlinked references/ folder is skipped entirely
|
||||
var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-folder-skip");
|
||||
// Assert — only assets/legit.md is found; the symlinked references/ directory is skipped entirely
|
||||
var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-directory-skip");
|
||||
Assert.NotNull(skill);
|
||||
Assert.Single(skill.Resources!);
|
||||
Assert.Equal("assets/legit.md", skill.Resources![0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_SymlinkedScriptFolder_SkipsWithoutEnumeratingAsync()
|
||||
public async Task GetSkillsAsync_SymlinkedScriptDirectory_SkipsWithoutEnumeratingAsync()
|
||||
{
|
||||
// Arrange — scripts/ is a symlink pointing outside the skill directory.
|
||||
// The directory-level check should skip it entirely.
|
||||
@@ -679,22 +679,22 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: symlink-script-skip\ndescription: Symlinked script folder\n---\nBody.");
|
||||
"---\nname: symlink-script-skip\ndescription: Symlinked script directory\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor);
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
|
||||
// Assert — skill loads but scripts from the symlinked folder are not discovered
|
||||
// Assert — skill loads but scripts from the symlinked directory are not discovered
|
||||
var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-script-skip");
|
||||
Assert.NotNull(skill);
|
||||
Assert.Empty(skill.Scripts!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_SymlinkedIntermediateSegment_SkipsCustomFolderAsync()
|
||||
public async Task GetSkillsAsync_SymlinkedIntermediateSegment_SkipsCustomDirectoryAsync()
|
||||
{
|
||||
// Arrange — custom resource folder "sub/resources" where "sub" is a symlink.
|
||||
// Arrange — custom resource directory "sub/resources" where "sub" is a symlink.
|
||||
// The directory-level HasSymlinkInPath check should detect the intermediate symlink.
|
||||
string skillDir = Path.Combine(this._testRoot, "symlink-intermediate");
|
||||
Directory.CreateDirectory(skillDir);
|
||||
@@ -720,12 +720,12 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
var source = new AgentFileSkillsSource(
|
||||
this._testRoot,
|
||||
s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ResourceFolders = ["sub/resources"] });
|
||||
new AgentFileSkillsSourceOptions { ResourceDirectories = ["sub/resources"] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
|
||||
// Assert — the symlinked intermediate segment causes the folder to be skipped
|
||||
// Assert — the symlinked intermediate segment causes the directory to be skipped
|
||||
var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "symlink-intermediate");
|
||||
Assert.NotNull(skill);
|
||||
Assert.Empty(skill.Resources!);
|
||||
@@ -900,11 +900,11 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
[InlineData("sub/../escape")]
|
||||
[InlineData("/absolute")]
|
||||
[InlineData("\\absolute")]
|
||||
public void Constructor_InvalidFolderName_SkipsInvalidFolders(string badFolder)
|
||||
public void Constructor_InvalidDirectoryName_SkipsInvalidDirectories(string badDirectory)
|
||||
{
|
||||
// Arrange & Act — invalid folders are skipped with a warning rather than throwing
|
||||
var source1 = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptFolders = [badFolder] });
|
||||
var source2 = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ResourceFolders = [badFolder] });
|
||||
// Arrange & Act — invalid directories are skipped with a warning rather than throwing
|
||||
var source1 = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptDirectories = [badDirectory] });
|
||||
var source2 = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ResourceDirectories = [badDirectory] });
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(source1);
|
||||
@@ -915,54 +915,54 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Constructor_NullOrWhitespaceFolderName_ThrowsArgumentException(string? badFolder)
|
||||
public void Constructor_NullOrWhitespaceDirectoryName_ThrowsArgumentException(string? badDirectory)
|
||||
{
|
||||
// Arrange & Act & Assert — null/whitespace is a contract violation, not a config error
|
||||
Assert.Throws<ArgumentException>(() => new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptFolders = [badFolder!] }));
|
||||
Assert.Throws<ArgumentException>(() => new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ResourceFolders = [badFolder!] }));
|
||||
Assert.Throws<ArgumentException>(() => new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptDirectories = [badDirectory!] }));
|
||||
Assert.Throws<ArgumentException>(() => new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ResourceDirectories = [badDirectory!] }));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("scripts")]
|
||||
[InlineData("my-scripts")]
|
||||
[InlineData("sub/folder")]
|
||||
[InlineData("sub/directory")]
|
||||
[InlineData(".")]
|
||||
[InlineData("./scripts")]
|
||||
[InlineData("./scripts/f1")]
|
||||
[InlineData("my..scripts")]
|
||||
public void Constructor_ValidFolderName_DoesNotThrow(string validFolder)
|
||||
public void Constructor_ValidDirectoryName_DoesNotThrow(string validDirectory)
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptFolders = [validFolder] });
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptDirectories = [validDirectory] });
|
||||
Assert.NotNull(source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_DuplicateFoldersAfterNormalization_NoDuplicateResourcesAsync()
|
||||
public async Task GetSkillsAsync_DuplicateDirectoriesAfterNormalization_NoDuplicateResourcesAsync()
|
||||
{
|
||||
// Arrange — "references" and "./references" refer to the same directory;
|
||||
// after normalization they should be deduplicated so resources appear only once.
|
||||
string skillDir = Path.Combine(this._testRoot, "dedup-folder-skill");
|
||||
string skillDir = Path.Combine(this._testRoot, "dedup-directory-skill");
|
||||
string refsDir = Path.Combine(skillDir, "references");
|
||||
Directory.CreateDirectory(refsDir);
|
||||
File.WriteAllText(Path.Combine(refsDir, "FAQ.md"), "FAQ content");
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: dedup-folder-skill\ndescription: Dedup test\n---\nBody.");
|
||||
"---\nname: dedup-directory-skill\ndescription: Dedup test\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ResourceFolders = ["references", "./references"] });
|
||||
new AgentFileSkillsSourceOptions { ResourceDirectories = ["references", "./references"] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
|
||||
// Assert — only one copy of the resource despite two equivalent folder entries
|
||||
// Assert — only one copy of the resource despite two equivalent directory entries
|
||||
Assert.Single(skills);
|
||||
Assert.Single(skills[0].Resources!);
|
||||
Assert.Equal("references/FAQ.md", skills[0].Resources![0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_TrailingSlashFolderNormalized_NoDuplicateResourcesAsync()
|
||||
public async Task GetSkillsAsync_TrailingSlashDirectoryNormalized_NoDuplicateResourcesAsync()
|
||||
{
|
||||
// Arrange — "references/" should be normalized to "references"
|
||||
string skillDir = Path.Combine(this._testRoot, "trailing-slash-skill");
|
||||
@@ -973,7 +973,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: trailing-slash-skill\ndescription: Trailing slash test\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ResourceFolders = ["references", "references/"] });
|
||||
new AgentFileSkillsSourceOptions { ResourceDirectories = ["references", "references/"] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
@@ -985,7 +985,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_BackslashFolderNormalized_NoDuplicateScriptsAsync()
|
||||
public async Task GetSkillsAsync_BackslashDirectoryNormalized_NoDuplicateScriptsAsync()
|
||||
{
|
||||
// Arrange — ".\\scripts" should be normalized to "scripts"
|
||||
string skillDir = Path.Combine(this._testRoot, "backslash-skill");
|
||||
@@ -996,7 +996,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: backslash-skill\ndescription: Backslash test\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ScriptFolders = ["scripts", ".\\scripts"] });
|
||||
new AgentFileSkillsSourceOptions { ScriptDirectories = ["scripts", ".\\scripts"] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
@@ -1010,48 +1010,48 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
[Theory]
|
||||
[InlineData("./references")]
|
||||
[InlineData("./assets/docs")]
|
||||
public async Task GetSkillsAsync_ResourceFolderWithDotSlashPrefix_DiscoversResourcesAsync(string folder)
|
||||
public async Task GetSkillsAsync_ResourceDirectoryWithDotSlashPrefix_DiscoversResourcesAsync(string directory)
|
||||
{
|
||||
// Arrange — "./references" and "./assets/docs" are equivalent to "references" and "assets/docs";
|
||||
// the leading "./" is transparently normalized by Path.GetFullPath during file enumeration.
|
||||
string folderWithoutDotSlash = folder.Substring(2); // strip "./"
|
||||
string directoryWithoutDotSlash = directory.Substring(2); // strip "./"
|
||||
string skillDir = Path.Combine(this._testRoot, "dotslash-res-skill");
|
||||
string targetDir = Path.Combine(skillDir, folderWithoutDotSlash.Replace('/', Path.DirectorySeparatorChar));
|
||||
string targetDir = Path.Combine(skillDir, directoryWithoutDotSlash.Replace('/', Path.DirectorySeparatorChar));
|
||||
Directory.CreateDirectory(targetDir);
|
||||
File.WriteAllText(Path.Combine(targetDir, "data.json"), "{}");
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: dotslash-res-skill\ndescription: Dot-slash prefix\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ResourceFolders = [folder] });
|
||||
new AgentFileSkillsSourceOptions { ResourceDirectories = [directory] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
|
||||
// Assert — the resource is discovered with a name identical to using the folder without "./"
|
||||
// Assert — the resource is discovered with a name identical to using the directory without "./"
|
||||
Assert.Single(skills);
|
||||
Assert.Single(skills[0].Resources!);
|
||||
Assert.Equal($"{folderWithoutDotSlash}/data.json", skills[0].Resources![0].Name);
|
||||
Assert.Equal($"{directoryWithoutDotSlash}/data.json", skills[0].Resources![0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_ResourceFoldersWithNestedPath_DiscoversResourcesAsync()
|
||||
public async Task GetSkillsAsync_ResourceDirectoriesWithNestedPath_DiscoversResourcesAsync()
|
||||
{
|
||||
// Arrange — ResourceFolders configured with a multi-segment relative path (f1/f2/f3)
|
||||
string skillDir = Path.Combine(this._testRoot, "nested-folder-skill");
|
||||
// Arrange — ResourceDirectories configured with a multi-segment relative path (f1/f2/f3)
|
||||
string skillDir = Path.Combine(this._testRoot, "nested-directory-skill");
|
||||
string nestedDir = Path.Combine(skillDir, "f1", "f2", "f3");
|
||||
Directory.CreateDirectory(nestedDir);
|
||||
File.WriteAllText(Path.Combine(nestedDir, "data.json"), "{}");
|
||||
File.WriteAllText(
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: nested-folder-skill\ndescription: Nested folder\n---\nBody.");
|
||||
"---\nname: nested-directory-skill\ndescription: Nested directory\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ResourceFolders = ["f1/f2/f3"] });
|
||||
new AgentFileSkillsSourceOptions { ResourceDirectories = ["f1/f2/f3"] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
|
||||
// Assert — resource file inside the deeply nested folder is discovered
|
||||
// Assert — resource file inside the deeply nested directory is discovered
|
||||
Assert.Single(skills);
|
||||
var skill = skills[0];
|
||||
Assert.Single(skill.Resources!);
|
||||
@@ -1107,9 +1107,9 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_ScriptInSkillRoot_DiscoveredWhenRootFolderConfiguredAsync()
|
||||
public async Task GetSkillsAsync_ScriptInSkillRoot_DiscoveredWhenRootDirectoryConfiguredAsync()
|
||||
{
|
||||
// Arrange — script file directly in the skill directory with ScriptFolders = ["."]
|
||||
// Arrange — script file directly in the skill directory with ScriptDirectories = ["."]
|
||||
string skillDir = Path.Combine(this._testRoot, "root-script-skill");
|
||||
Directory.CreateDirectory(skillDir);
|
||||
File.WriteAllText(Path.Combine(skillDir, "run.py"), "print('hello')");
|
||||
@@ -1117,7 +1117,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
Path.Combine(skillDir, "SKILL.md"),
|
||||
"---\nname: root-script-skill\ndescription: Root script\n---\nBody.");
|
||||
var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor,
|
||||
new AgentFileSkillsSourceOptions { ScriptFolders = ["."] });
|
||||
new AgentFileSkillsSourceOptions { ScriptDirectories = ["."] });
|
||||
|
||||
// Act
|
||||
var skills = await source.GetSkillsAsync();
|
||||
@@ -1131,7 +1131,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
|
||||
|
||||
#if NET
|
||||
[Fact]
|
||||
public async Task GetSkillsAsync_SymlinkedFileInRealFolder_SkipsSymlinkedFileAsync()
|
||||
public async Task GetSkillsAsync_SymlinkedFileInRealDirectory_SkipsSymlinkedFileAsync()
|
||||
{
|
||||
// Arrange — references/ is a real directory, but one file inside it is a symlink
|
||||
// pointing outside the skill directory. The per-file symlink check should skip it.
|
||||
|
||||
Reference in New Issue
Block a user