Compare commits

...
Author SHA1 Message Date
Jacob Alber 45166cdaa5 fix: address review comments 2026-04-03 13:56:03 -04:00
Jacob Alber 32266a5267 fix: ResumeAsync does not run pending steps 2026-04-03 13:56:00 -04:00
Jacob Alber bd74ce7f8f feat: Implement Executor Message Delivery Event callbacks 2026-04-03 13:55:58 -04:00
Tao ChenGitHubTaoChenOSUcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
e94cfc6aef Python: Remove pre-release flag from agent-framework installation (#5082)
* Remove pre-release flag from agent-framework installation

* README: remove --pre from Python Quickstart pip install comment

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/c2444957-235e-43a1-9777-df9fdf12919b

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
2026-04-03 16:47:23 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d1a81159de Bump Anthropic from 12.8.0 to 12.11.0 (#5055)
---
updated-dependencies:
- dependency-name: Anthropic
  dependency-version: 12.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 15:48:59 +00:00
Peter IbekweandGitHub 9f0dbe5f8d .NET: Improve workflow unit test coverage (#5072)
* Improve workflow unit tests

* Update test name prefix for clarity.

* Update tests to surface any errors.
2026-04-03 15:04:59 +00:00
3fc1d00026 .NET: skill as class (#5027)
* add class-based skills

* address formating issues

* Remove generated filtered-unit.slnx and add to .gitignore

The filtered solution file is generated dynamically by
eng/scripts/New-FilteredSolution.ps1 during CI. Checking it in
risks it becoming stale and out-of-sync with the real solution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove generated filtered-unit.slnx and add to .gitignore

The filtered solution file is generated dynamically by
eng/scripts/New-FilteredSolution.ps1 during CI. Checking it in
risks it becoming stale and out-of-sync with the real solution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* consolidate DI samples into one

* fix file encoding

* suppress compatibility warning

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-03 11:27:36 +00:00
westeyandGitHub e4defadc79 .NET: Add github actions workflow for verify-samples (#5034)
* Add github actions workflow for verify-samples

* Make workflow run as part of PR (for now)

* Update workflow to remove pr trigger

* Address PR comments
2026-04-03 09:58:24 +00:00
Jacob AlberandGitHub c798cb7a2e release: Mark Handoff Orchestrations Experimental (#5065) 2026-04-02 15:48:19 +00:00
Eduard van ValkenburgandGitHub 3446eb8d5d Python: [BREAKING] update to v1.0.0 (#5062)
* updates to final deprecated pieces and versions

* fix mypy

* fix readme links
2026-04-02 15:26:30 +00:00
5f06b68535 Python: handle streamed A2A update events (#4919)
* Python: handle streamed A2A update events

* Python: preserve terminal A2A artifacts during streaming

* Python: harden streamed A2A update event handling

* Python: simplify streamed A2A update guard

---------

Co-authored-by: sztoplover-bit <253473756+sztoplover-bit@users.noreply.github.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
2026-04-02 15:20:47 +00:00
westeyandGitHub 524c0216e4 .NET: Update release versions (#5059)
* Update release versions

* Fix versioning issue

* Change ga wording to released

* Update suppression file

* Remove unecessary workflows suppression file

* Remove another unnecessary suppression file.

* Fix suppressions file
2026-04-02 14:54:28 +00:00
Jacob AlberandGitHub 281661e409 .NET: Remove timeout from InputWait in OffThread execution (#4996)
* fix: Remove Timeout from InputWait in StreamingRunEventStream

* fix: Race condition when the workflow executes to halt before TakeEventStream

* test: Make the OffThread Delay test more nimble

* fix: Remove slight window where runStatus could be stale
2026-04-02 14:35:25 +00:00
Roger BarretoandGitHub b0613a8ceb .NET: Bump Azure.AI.Projects to 2.0.0 GA (#5060)
* Bump Azure.AI.Projects to 2.0.0 GA

- Update Azure.AI.Projects from 2.0.0-beta.2 to 2.0.0 in CPM
- Update Azure.Identity from 1.19.0 to 1.20.0 (transitive dep)
- Update System.ClientModel from 1.9.0 to 1.10.0 (transitive dep)
- Rename types per Azure.AI.Projects.Agents 2.0.0 breaking changes:
  - AgentVersion -> ProjectsAgentVersion
  - AgentRecord -> ProjectsAgentRecord
  - AgentDefinition -> ProjectsAgentDefinition
  - AgentVersionCreationOptions -> ProjectsAgentVersionCreationOptions
  - PromptAgentDefinition -> DeclarativeAgentDefinition
  - AgentTool -> ProjectsAgentTool
  - AgentsClient -> AgentAdministrationClient
  - .Agents property -> .AgentAdministrationClient
- Add using Azure.AI.Projects.Memory namespace (types moved)
- Update AGENTS.md with BOM and output capture conventions

* Address PR review feedback

- Rename AIProjectClient parameter to aiProjectClient in AsChatClientAgent overloads
- Fix XML doc: ProjectsAgentTool namespace from Azure.AI.Projects.OpenAI to Azure.AI.Projects.Agents
- Rename test method to reflect DeclarativeAgentDefinition terminology
2026-04-02 14:02:29 +00:00
Jacob AlberandGitHub 79b38040e8 fix: Update Google.GenAI to verison compatible with M.E.AI 10.4.0+ (#5061) 2026-04-02 13:57:40 +00:00
a356a16568 .NET: Remove OpenAIAssistantClientExtensions class (#5058)
* Remove OpenAIAssistantClientExtensions class

Remove the deprecated OpenAI Assistants API extension methods, along with
their unit tests, integration tests, sample project, and related references.

- Delete OpenAIAssistantClientExtensions.cs (source)
- Delete OpenAIAssistantClientExtensionsTests.cs (unit + integration tests)
- Delete Agent_With_OpenAIAssistants sample project
- Remove sample from solution file, README, and verify-samples definitions
- Remove AIOpenAIAssistants diagnostic ID constant

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* add removed extension methods to the suppression file

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 13:51:41 +00:00
276 changed files with 5763 additions and 5072 deletions
+122
View File
@@ -0,0 +1,122 @@
#
# Runs the .NET sample verification tool, which builds and executes sample projects
# and verifies their output using deterministic checks and AI-powered verification.
#
# Results are displayed as a GitHub Job Summary and the CSV report is uploaded as an artifact.
#
name: dotnet-verify-samples
on:
workflow_dispatch:
inputs:
category:
description: "Sample category to run (blank for all)"
required: false
type: choice
options:
- ""
- "01-get-started"
- "02-agents"
- "03-workflows"
parallelism:
description: "Max parallel sample runs"
required: false
default: "8"
type: string
schedule:
- cron: "0 6 * * 1-5" # Weekdays at 6:00 UTC
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
jobs:
verify-samples:
runs-on: ubuntu-latest
environment: 'integration'
timeout-minutes: 90
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
workflow-samples
- name: Setup dotnet
uses: actions/setup-dotnet@v5.2.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Run verify-samples
id: verify
working-directory: dotnet
shell: bash
run: |
CATEGORY_ARG=""
if [ -n "$CATEGORY_INPUT" ]; then
CATEGORY_ARG="--category $CATEGORY_INPUT"
fi
dotnet run --project eng/verify-samples -- \
$CATEGORY_ARG \
--parallel "$PARALLELISM" \
--md results.md \
--csv results.csv \
--log results.log
env:
CATEGORY_INPUT: ${{ github.event.inputs.category || '' }}
PARALLELISM: ${{ github.event.inputs.parallelism || '8' }}
# OpenAI Models
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_MODEL_NAME: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_REASONING_MODEL_NAME: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
# Azure OpenAI Models
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Azure AI Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
- name: Write Job Summary
if: always()
working-directory: dotnet
shell: bash
run: |
if [ -f results.md ]; then
cat results.md >> "$GITHUB_STEP_SUMMARY"
else
echo "⚠️ No results.md generated — verify-samples may have failed to start." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload results
if: always()
uses: actions/upload-artifact@v7
with:
name: verify-samples-results
path: |
dotnet/results.csv
dotnet/results.log
if-no-files-found: warn
- name: Fail if samples failed
if: always() && steps.verify.outcome == 'failure'
shell: bash
run: exit 1
+3
View File
@@ -230,3 +230,6 @@ local.settings.json
# Database files
*.db
python/dotnet-ref
# Generated filtered solution files (created by eng/scripts/New-FilteredSolution.ps1)
dotnet/filtered-*.slnx
+2 -2
View File
@@ -28,7 +28,7 @@ Welcome to Microsoft's comprehensive multi-language framework for building, orch
Python
```bash
pip install agent-framework --pre
pip install agent-framework
# This will install all sub-packages, see `python/packages` for individual packages.
# It may take a minute on first install on Windows.
```
@@ -90,7 +90,7 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
Create a simple Azure Responses Agent that writes a haiku about the Microsoft Agent Framework
```python
# pip install agent-framework --pre
# pip install agent-framework
# Use `az login` to authenticate with Azure CLI
import os
import asyncio
+2 -1
View File
@@ -25,7 +25,7 @@ dotnet run --project eng/verify-samples -- Agent_Step02_StructuredOutput Agent_S
dotnet run --project eng/verify-samples -- --parallel 8 --log results.log
# Combine options
dotnet run --project eng/verify-samples -- --category 03-workflows --parallel 4 --log results.log --csv results.csv
dotnet run --project eng/verify-samples -- --category 03-workflows --parallel 4 --log results.log --csv results.csv --md results.md
```
### Required Environment Variables
@@ -40,6 +40,7 @@ Individual samples require their own env vars (e.g., `AZURE_AI_PROJECT_ENDPOINT`
- `--log results.log` — detailed per-sample log with stdout/stderr, AI reasoning, and a summary
- `--csv results.csv` — tabular summary with Sample, ProjectPath, Status, FailedChecks, and Failures columns
- `--md results.md` — Markdown summary with results table and collapsible failure details (suitable for GitHub PR comments)
## Sample Categories
+3 -2
View File
@@ -29,13 +29,14 @@ using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, `AIFunct
## Key Conventions
- **Encoding**: All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly.
- **Command output capture**: When running `dotnet build`, `dotnet test`, `dotnet format`, or similar commands, redirect output to a temp file first (e.g., `dotnet build --tl:off 2>&1 | Out-File $env:TEMP\build.log`), then analyze the file as needed. This avoids re-running expensive commands when the initial analysis misses something.
- **Encoding**: All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly. When using PowerShell `Set-Content`, always pass `-Encoding UTF8BOM` to preserve the BOM (e.g., `Set-Content $file $content -NoNewline -Encoding UTF8BOM`).
- **Copyright header**: `// Copyright (c) Microsoft. All rights reserved.` at top of all `.cs` files
- **XML docs**: Required for all public methods and classes
- **Async**: Use `Async` suffix for methods returning `Task`/`ValueTask`
- **Private classes**: Should be `sealed` unless subclassed
- **Config**: Read from environment variables with `UPPER_SNAKE_CASE` naming
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking; test methods returning `Task`/`ValueTask` must use the `Async` suffix.
## Key Design Principles
+1 -1
View File
@@ -17,7 +17,7 @@
<PropertyGroup>
<IsReleaseCandidate>false</IsReleaseCandidate>
<IsGenerallyAvailable>false</IsGenerallyAvailable>
<IsReleased>false</IsReleased>
</PropertyGroup>
<PropertyGroup>
+5 -5
View File
@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.8.0" />
<PackageVersion Include="Anthropic" Version="12.11.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.4.2" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
@@ -19,13 +19,13 @@
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0-beta.2" />
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.19.0" />
<PackageVersion Include="Azure.Identity" Version="1.20.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- Google Gemini -->
<PackageVersion Include="Google.GenAI" Version="0.11.0" />
<PackageVersion Include="Google.GenAI" Version="1.6.0" />
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
<!-- Microsoft.Azure.* -->
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
@@ -35,7 +35,7 @@
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.4" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.4" />
<PackageVersion Include="System.ClientModel" Version="1.9.0" />
<PackageVersion Include="System.ClientModel" Version="1.10.0" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
+3 -1
View File
@@ -34,7 +34,6 @@
<Project Path="samples/02-agents/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIAssistants/Agent_With_OpenAIAssistants.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
</Folder>
@@ -107,6 +106,9 @@
<File Path="samples/02-agents/AgentSkills/README.md" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Agent_Step02_CodeDefinedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
@@ -781,19 +781,6 @@ internal static class AgentsSamples
SkipReason = "Requires local Ollama server.",
},
new SampleDefinition
{
Name = "Agent_With_OpenAIAssistants",
ProjectPath = "samples/02-agents/AgentProviders/Agent_With_OpenAIAssistants",
RequiredEnvironmentVariables = ["OPENAI_API_KEY"],
OptionalEnvironmentVariables = ["OPENAI_CHAT_MODEL_NAME"],
ExpectedOutputDescription =
[
"The output should contain a joke about a pirate from the OpenAI Assistants API.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Agent_With_OpenAIChatCompletion",
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
namespace VerifySamples;
/// <summary>
/// Writes a Markdown summary of sample verification results.
/// </summary>
internal static class MarkdownResultWriter
{
/// <summary>
/// Writes the results to a Markdown file at the specified path.
/// </summary>
public static async Task WriteAsync(
string path,
IReadOnlyList<VerificationResult> orderedResults,
IReadOnlyList<(string Name, string Reason)> skipped,
TimeSpan elapsed)
{
var passCount = orderedResults.Count(r => r.Passed);
var failCount = orderedResults.Count(r => !r.Passed);
var sb = new StringBuilder();
sb.AppendLine("# Sample Verification Results");
sb.AppendLine();
sb.AppendLine($"**{passCount} passed, {failCount} failed, {skipped.Count} skipped** | Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
sb.AppendLine();
// Results table
sb.AppendLine("| Sample | Status | Failed Checks | Failures |");
sb.AppendLine("|--------|--------|---------------|----------|");
foreach (var result in orderedResults)
{
var status = result.Passed ? "✅ PASSED" : "❌ FAILED";
var failedChecks = result.Failures.Count;
var failures = MdEscape(string.Join("; ", result.Failures));
sb.AppendLine($"| {MdEscape(result.SampleName)} | {status} | {failedChecks} | {failures} |");
}
foreach (var (name, reason) in skipped)
{
sb.AppendLine($"| {MdEscape(name)} | ⏭️ SKIPPED | 0 | {MdEscape(reason)} |");
}
// Collapsible AI reasoning details for failures
var failures2 = orderedResults.Where(r => !r.Passed && !string.IsNullOrEmpty(r.AIReasoning)).ToList();
if (failures2.Count > 0)
{
sb.AppendLine();
sb.AppendLine("## Failure Details");
sb.AppendLine();
foreach (var result in failures2)
{
sb.AppendLine($"<details><summary><strong>{HtmlEscape(result.SampleName)}</strong></summary>");
sb.AppendLine();
if (result.Failures.Count > 0)
{
foreach (var failure in result.Failures)
{
sb.AppendLine($"- {MdEscape(failure)}");
}
sb.AppendLine();
}
sb.AppendLine("**AI Reasoning:**");
sb.AppendLine();
sb.AppendLine("```");
sb.AppendLine(result.AIReasoning);
sb.AppendLine("```");
sb.AppendLine();
sb.AppendLine("</details>");
sb.AppendLine();
}
}
await File.WriteAllTextAsync(path, sb.ToString());
}
/// <summary>
/// Escapes pipe characters and newlines for use inside Markdown table cells.
/// </summary>
private static string MdEscape(string value)
{
return value.Replace("|", "\\|").Replace("\n", " ").Replace("\r", "");
}
/// <summary>
/// Escapes HTML special characters for use inside HTML tags.
/// </summary>
private static string HtmlEscape(string value)
{
return value.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
}
}
+8
View File
@@ -13,6 +13,7 @@
// dotnet run -- --parallel 16 # Run up to 16 samples concurrently
// dotnet run -- --log results.log # Write sequential log to file
// dotnet run -- --csv results.csv # Write CSV summary to file
// dotnet run -- --md results.md # Write Markdown summary to file
//
// Required environment variables (for AI-powered samples):
// AZURE_OPENAI_ENDPOINT
@@ -90,6 +91,13 @@ try
Console.WriteLine($"CSV written to: {options.CsvFilePath}");
}
// Write Markdown summary
if (options.MarkdownFilePath is not null)
{
await MarkdownResultWriter.WriteAsync(options.MarkdownFilePath, orderedResults, run.Skipped, stopwatch.Elapsed);
Console.WriteLine($"Markdown written to: {options.MarkdownFilePath}");
}
return orderedResults.Any(r => !r.Passed) ? 1 : 0;
}
finally
@@ -17,6 +17,11 @@ internal sealed class VerifyOptions
/// </summary>
public string? CsvFilePath { get; init; }
/// <summary>
/// Path to write a Markdown summary file, or <c>null</c> to skip.
/// </summary>
public string? MarkdownFilePath { get; init; }
/// <summary>
/// Path to write a sequential log file, or <c>null</c> to skip.
/// </summary>
@@ -49,6 +54,7 @@ internal sealed class VerifyOptions
var categoryFilter = ExtractArg(argList, "--category");
var logFilePath = ExtractArg(argList, "--log");
var csvFilePath = ExtractArg(argList, "--csv");
var markdownFilePath = ExtractArg(argList, "--md");
int maxParallelism = 8;
var parallelArg = ExtractArg(argList, "--parallel");
@@ -98,6 +104,7 @@ internal sealed class VerifyOptions
MaxParallelism = maxParallelism,
LogFilePath = logFilePath,
CsvFilePath = csvFilePath,
MarkdownFilePath = markdownFilePath,
Samples = samples,
};
}
+7 -6
View File
@@ -2,19 +2,20 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<RCNumber>5</RCNumber>
<RCNumber>6</RCNumber>
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260330.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260330.1</PackageVersion>
<GitTag>1.0.0-rc5</GitTag>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260402.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260402.1</PackageVersion>
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
<GitTag>1.0.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-rc4</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>1.0.0-rc5</PackageValidationBaselineVersion>
<!-- Enable validation for RC packages and GA packages -->
<EnablePackageValidation Condition="'$(IsReleaseCandidate)' == 'true' OR '$(IsGenerallyAvailable)' == 'true'">true</EnablePackageValidation>
<EnablePackageValidation Condition="'$(IsReleaseCandidate)' == 'true' OR '$(IsReleased)' == 'true'">true</EnablePackageValidation>
<!-- Validate assembly attributes only for Publish builds -->
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
<!-- Do not validate reference assemblies -->
@@ -20,10 +20,10 @@ const string JokerName = "JokerAgent";
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// Define the agent you want to create. (Prompt Agent in this case)
var agentVersionCreationOptions = new AgentVersionCreationOptions(new PromptAgentDefinition(model: deploymentName) { Instructions = "You are good at telling jokes." });
var agentVersionCreationOptions = new ProjectsAgentVersionCreationOptions(new DeclarativeAgentDefinition(model: deploymentName) { Instructions = "You are good at telling jokes." });
// Azure.AI.Agents SDK creates and manages agent by name and versions.
// You can create a server side agent version with the Azure.AI.Agents SDK client below.
var createdAgentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName: JokerName, options: agentVersionCreationOptions);
var createdAgentVersion = aiProjectClient.AgentAdministrationClient.CreateAgentVersion(agentName: JokerName, options: agentVersionCreationOptions);
// Note:
// agentVersion.Id = "<agentName>:<versionNumber>",
@@ -34,15 +34,15 @@ var createdAgentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName: J
FoundryAgent existingJokerAgent = aiProjectClient.AsAIAgent(createdAgentVersion);
// You can also create another AIAgent version by providing the same name with a different definition.
AgentVersion newJokerAgentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
ProjectsAgentVersion newJokerAgentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
JokerName,
new AgentVersionCreationOptions(new PromptAgentDefinition(model: deploymentName) { Instructions = "You are extremely hilarious at telling jokes." }));
new ProjectsAgentVersionCreationOptions(new DeclarativeAgentDefinition(model: deploymentName) { Instructions = "You are extremely hilarious at telling jokes." }));
FoundryAgent newJokerAgent = aiProjectClient.AsAIAgent(newJokerAgentVersion);
// You can also get the AIAgent latest version just providing its name.
AgentRecord jokerAgentRecord = await aiProjectClient.Agents.GetAgentAsync(JokerName);
ProjectsAgentRecord jokerAgentRecord = await aiProjectClient.AgentAdministrationClient.GetAgentAsync(JokerName);
FoundryAgent jokerAgentLatest = aiProjectClient.AsAIAgent(jokerAgentRecord);
AgentVersion latestAgentVersion = jokerAgentRecord.GetLatestVersion();
ProjectsAgentVersion latestAgentVersion = jokerAgentRecord.GetLatestVersion();
// The AIAgent version can be accessed via the GetService method.
Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}");
@@ -55,4 +55,4 @@ Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate
Console.WriteLine(await jokerAgentLatest.RunAsync("Now tell me a joke about a cat and a dog using last joke as the anchor.", session));
// Cleanup by agent name removes both agent versions created.
aiProjectClient.Agents.DeleteAgent(existingJokerAgent.Name);
aiProjectClient.AgentAdministrationClient.DeleteAgent(existingJokerAgent.Name);
@@ -1,41 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with OpenAI Assistants as the backend.
// WARNING: The Assistants API is deprecated and will be shut down.
// For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration
#pragma warning disable CS0618 // Type or member is obsolete - OpenAI Assistants API is deprecated but still used in this sample
using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Assistants;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-4o-mini";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Get a client to create/retrieve server side agents with.
var assistantClient = new OpenAIClient(apiKey).GetAssistantClient();
// You can create a server side assistant with the OpenAI SDK.
var createResult = await assistantClient.CreateAssistantAsync(model, new() { Name = JokerName, Instructions = JokerInstructions });
// You can retrieve an already created server side assistant as an AIAgent.
AIAgent agent1 = await assistantClient.GetAIAgentAsync(createResult.Value.Id);
// You can also create a server side assistant and return it as an AIAgent directly.
AIAgent agent2 = await assistantClient.CreateAIAgentAsync(
model: model,
name: JokerName,
instructions: JokerInstructions);
// You can invoke the agent like any other AIAgent.
AgentSession session = await agent1.CreateSessionAsync();
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session));
// Cleanup for sample purposes.
await assistantClient.DeleteAssistantAsync(agent1.Id);
await assistantClient.DeleteAssistantAsync(agent2.Id);
@@ -1,16 +0,0 @@
# Prerequisites
WARNING: The Assistants API is deprecated and will be shut down.
For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration
Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- OpenAI API key
Set the following environment variables:
```powershell
$env:OPENAI_API_KEY="*****" # Replace with your OpenAI API key
$env:OPENAI_CHAT_MODEL_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
@@ -25,7 +25,6 @@ See the README.md for each sample for the prerequisites for that sample.
|[Creating an AIAgent with GitHub Copilot](./Agent_With_GitHubCopilot/)|This sample demonstrates how to create an AIAgent using GitHub Copilot SDK as the underlying inference service|
|[Creating an AIAgent with Ollama](./Agent_With_Ollama/)|This sample demonstrates how to create an AIAgent using Ollama as the underlying inference service|
|[Creating an AIAgent with ONNX](./Agent_With_ONNX/)|This sample demonstrates how to create an AIAgent using ONNX as the underlying inference service|
|[Creating an AIAgent with OpenAI Assistants](./Agent_With_OpenAIAssistants/)|This sample demonstrates how to create an AIAgent using OpenAI Assistants as the underlying inference service.</br>WARNING: The Assistants API is deprecated and will be shut down. For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration|
|[Creating an AIAgent with OpenAI ChatCompletion](./Agent_With_OpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using OpenAI ChatCompletion as the underlying inference service|
|[Creating an AIAgent with OpenAI Responses](./Agent_With_OpenAIResponses/)|This sample demonstrates how to create an AIAgent using OpenAI Responses as the underlying inference service|
@@ -6,7 +6,7 @@ This sample demonstrates how to use **file-based Agent Skills** with a `ChatClie
- Discovering skills from `SKILL.md` files on disk via `AgentFileSkillsSource`
- The progressive disclosure pattern: advertise → load → read resources → run scripts
- Using the `AgentSkillsProvider` constructor with a skill directory path and script executor
- Using the `AgentSkillsProvider` constructor with a skill directory path and script runner
- Running file-based scripts (Python) via a subprocess-based executor
## Skills Included
@@ -6,8 +6,14 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to define Agent Skills as C# classes using AgentClassSkill.
// Class-based skills bundle all components into a single class implementation.
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// --- Class-Based Skill ---
// Instantiate the skill class.
var unitConverter = new UnitConverterSkill();
// --- Skills Provider ---
var skillsProvider = new AgentSkillsProvider(unitConverter);
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
// --- Example: Unit conversion ---
Console.WriteLine("Converting units with class-based skills");
Console.WriteLine(new string('-', 60));
AgentResponse response = await agent.RunAsync(
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
Console.WriteLine($"Agent: {response.Text}");
/// <summary>
/// A unit-converter skill defined as a C# class.
/// </summary>
/// <remarks>
/// Class-based skills bundle all components (name, description, body, resources, scripts)
/// into a single class.
/// </remarks>
internal sealed class UnitConverterSkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillResource>? _resources;
private IReadOnlyList<AgentSkillScript>? _scripts;
/// <inheritdoc/>
public override AgentSkillFrontmatter Frontmatter { get; } = new(
"unit-converter",
"Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.");
/// <inheritdoc/>
protected override string Instructions => """
Use this skill when the user asks to convert between units.
1. Review the conversion-table resource to find the factor for the requested conversion.
2. Use the convert script, passing the value and factor from the table.
3. Present the result clearly with both units.
""";
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
[
CreateResource(
"conversion-table",
"""
# Conversion Tables
Formula: **result = value × factor**
| From | To | Factor |
|-------------|-------------|----------|
| miles | kilometers | 1.60934 |
| kilometers | miles | 0.621371 |
| pounds | kilograms | 0.453592 |
| kilograms | pounds | 2.20462 |
"""),
];
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
[
CreateScript("convert", ConvertUnits),
];
private static string ConvertUnits(double value, double factor)
{
double result = Math.Round(value * factor, 4);
return JsonSerializer.Serialize(new { value, factor, result });
}
}
@@ -0,0 +1,49 @@
# Class-Based Agent Skills Sample
This sample demonstrates how to define **Agent Skills as C# classes** using `AgentClassSkill`.
## What it demonstrates
- Creating skills as classes that extend `AgentClassSkill`
- Bundling name, description, body, resources, and scripts into a single class
- Using the `AgentSkillsProvider` constructor with class-based skills
## Skills Included
### unit-converter (class-based)
A `UnitConverterSkill` class that converts between common units. Defined in `Program.cs`:
- `conversion-table` — Static resource with factor table
- `convert` — Script that performs `value × factor` conversion
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
### Setup
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```
### Run
```bash
dotnet run
```
### Expected Output
```
Converting units with class-based skills
------------------------------------------------------------
Agent: Here are your conversions:
1. **26.2 miles → 42.16 km** (a marathon distance)
2. **75 kg → 165.35 lbs**
```
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
<!-- Copy skills directory to output -->
<ItemGroup>
<None Include="skills\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,149 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates an advanced scenario: combining multiple skill types in a single agent
// using AgentSkillsProviderBuilder. The builder is designed for cases where the simple
// AgentSkillsProvider constructors are insufficient — for example, when you need to mix skill
// sources, apply filtering, or configure cross-cutting options in one place.
//
// Three different skill sources are registered here:
// 1. File-based: unit-converter (miles↔km, pounds↔kg) from SKILL.md on disk
// 2. Code-defined: volume-converter (gallons↔liters) using AgentInlineSkill
// 3. Class-based: temperature-converter (°F↔°C↔K) using AgentClassSkill
//
// For simpler, single-source scenarios, see the earlier steps in this sample series
// (e.g., Step01 for file-based, Step02 for code-defined, Step03 for class-based).
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// --- 1. Code-Defined Skill: volume-converter ---
var volumeConverterSkill = new AgentInlineSkill(
name: "volume-converter",
description: "Convert between gallons and liters using a multiplication factor.",
instructions: """
Use this skill when the user asks to convert between gallons and liters.
1. Review the volume-conversion-table resource to find the correct factor.
2. Use the convert-volume script, passing the value and factor.
""")
.AddResource("volume-conversion-table",
"""
# Volume Conversion Table
Formula: **result = value × factor**
| From | To | Factor |
|---------|---------|---------|
| gallons | liters | 3.78541 |
| liters | gallons | 0.264172|
""")
.AddScript("convert-volume", (double value, double factor) =>
{
double result = Math.Round(value * factor, 4);
return JsonSerializer.Serialize(new { value, factor, result });
});
// --- 2. Class-Based Skill: temperature-converter ---
var temperatureConverter = new TemperatureConverterSkill();
// --- 3. Build provider combining all three source types ---
var skillsProvider = new AgentSkillsProviderBuilder()
.UseFileSkill(Path.Combine(AppContext.BaseDirectory, "skills")) // File-based: unit-converter
.UseSkill(volumeConverterSkill) // Code-defined: volume-converter
.UseSkill(temperatureConverter) // Class-based: temperature-converter
.UseFileScriptRunner(SubprocessScriptRunner.RunAsync)
.Build();
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "MultiConverterAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant that can convert units, volumes, and temperatures.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
// --- Example: Use all three skills ---
Console.WriteLine("Converting with mixed skills (file + code + class)");
Console.WriteLine(new string('-', 60));
AgentResponse response = await agent.RunAsync(
"I need three conversions: " +
"1) How many kilometers is a marathon (26.2 miles)? " +
"2) How many liters is a 5-gallon bucket? " +
"3) What is 98.6°F in Celsius?");
Console.WriteLine($"Agent: {response.Text}");
/// <summary>
/// A temperature-converter skill defined as a C# class.
/// </summary>
internal sealed class TemperatureConverterSkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillResource>? _resources;
private IReadOnlyList<AgentSkillScript>? _scripts;
/// <inheritdoc/>
public override AgentSkillFrontmatter Frontmatter { get; } = new(
"temperature-converter",
"Convert between temperature scales (Fahrenheit, Celsius, Kelvin).");
/// <inheritdoc/>
protected override string Instructions => """
Use this skill when the user asks to convert temperatures.
1. Review the temperature-conversion-formulas resource for the correct formula.
2. Use the convert-temperature script, passing the value, source scale, and target scale.
3. Present the result clearly with both temperature scales.
""";
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
[
CreateResource(
"temperature-conversion-formulas",
"""
# Temperature Conversion Formulas
| From | To | Formula |
|-------------|-------------|---------------------------|
| Fahrenheit | Celsius | °C = (°F 32) × 5/9 |
| Celsius | Fahrenheit | °F = (°C × 9/5) + 32 |
| Celsius | Kelvin | K = °C + 273.15 |
| Kelvin | Celsius | °C = K 273.15 |
"""),
];
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
[
CreateScript("convert-temperature", ConvertTemperature),
];
private static string ConvertTemperature(double value, string from, string to)
{
double result = (from.ToUpperInvariant(), to.ToUpperInvariant()) switch
{
("FAHRENHEIT", "CELSIUS") => Math.Round((value - 32) * 5.0 / 9.0, 2),
("CELSIUS", "FAHRENHEIT") => Math.Round(value * 9.0 / 5.0 + 32, 2),
("CELSIUS", "KELVIN") => Math.Round(value + 273.15, 2),
("KELVIN", "CELSIUS") => Math.Round(value - 273.15, 2),
_ => throw new ArgumentException($"Unsupported conversion: {from} → {to}")
};
return JsonSerializer.Serialize(new { value, from, to, result });
}
}
@@ -0,0 +1,67 @@
# Mixed Agent Skills Sample (Advanced)
This sample demonstrates an **advanced scenario**: combining multiple skill types in a single agent using `AgentSkillsProviderBuilder`.
> **Tip:** For simpler, single-source scenarios, use the `AgentSkillsProvider` constructors directly — see [Step01](../Agent_Step01_FileBasedSkills/) (file-based), [Step02](../Agent_Step02_CodeDefinedSkills/) (code-defined), or [Step03](../Agent_Step03_ClassBasedSkills/) (class-based).
## What it demonstrates
- Combining file-based, code-defined, and class-based skills in one provider
- Using `UseFileSkill` and `UseSkill` on the builder to register different skill types
- Aggregating skills from all sources into a single provider with automatic deduplication
## When to use `AgentSkillsProviderBuilder`
The builder is intended for advanced scenarios where the simple `AgentSkillsProvider` constructors are insufficient:
| Scenario | Builder method |
|----------|---------------|
| **Mixed skill types** — combine file-based, code-defined, and class-based skills | `UseFileSkill` + `UseSkill` / `UseSkills` |
| **Multiple file script runners** — use different script runners for different file skill directories | `UseFileSkill` / `UseFileSkills` with per-source `scriptRunner` |
| **Skill filtering** — include/exclude skills using a predicate | `UseFilter(predicate)` |
## Skills Included
### unit-converter (file-based)
Discovered from `skills/unit-converter/SKILL.md` on disk. Converts miles↔km, pounds↔kg.
### volume-converter (code-defined)
Defined as `AgentInlineSkill` in `Program.cs`. Converts gallons↔liters.
### temperature-converter (class-based)
Defined as `TemperatureConverterSkill` class in `Program.cs`. Converts °F↔°C↔K.
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
### Setup
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```
### Run
```bash
dotnet run
```
### Expected Output
```
Converting with mixed skills (file + code + class)
------------------------------------------------------------
Agent: Here are your conversions:
1. **26.2 miles → 42.16 km** (a marathon distance)
2. **5 gallons → 18.93 liters**
3. **98.6°F → 37.0°C**
```
@@ -0,0 +1,11 @@
---
name: unit-converter
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
---
## Usage
When the user requests a unit conversion:
1. First, review `references/unit-conversion-table.md` to find the correct factor
2. Run the `scripts/convert-units.py` script with `--value <number> --factor <factor>` (e.g. `--value 26.2 --factor 1.60934`)
3. Present the converted value clearly with both units
@@ -0,0 +1,10 @@
# Conversion Tables
Formula: **result = value × factor**
| From | To | Factor |
|-------------|-------------|----------|
| miles | kilometers | 1.60934 |
| kilometers | miles | 0.621371 |
| pounds | kilograms | 0.453592 |
| kilograms | pounds | 2.20462 |
@@ -0,0 +1,29 @@
# Unit conversion script
# Converts a value using a multiplication factor: result = value × factor
#
# Usage:
# python scripts/convert-units.py --value 26.2 --factor 1.60934
# python scripts/convert-units.py --value 75 --factor 2.20462
import argparse
import json
def main() -> None:
parser = argparse.ArgumentParser(
description="Convert a value using a multiplication factor.",
epilog="Examples:\n"
" python scripts/convert-units.py --value 26.2 --factor 1.60934\n"
" python scripts/convert-units.py --value 75 --factor 2.20462",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.")
parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.")
args = parser.parse_args()
result = round(args.value * args.factor, 4)
print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
if __name__ == "__main__":
main()
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001;CA1812</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,208 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use Dependency Injection (DI) with Agent Skills.
// It shows two approaches side-by-side, each handling a different conversion domain:
//
// 1. Code-defined skill (AgentInlineSkill) — converts distances (miles ↔ kilometers).
// Resources and scripts are inline delegates that resolve services from IServiceProvider.
//
// 2. Class-based skill (AgentClassSkill) — converts weights (pounds ↔ kilograms).
// Resources and scripts are encapsulated in a class, also resolving services from IServiceProvider.
//
// Both skills share the same ConversionService registered in the DI container,
// showing that DI works identically regardless of how the skill is defined.
// When prompted with a question spanning both domains, the agent uses both skills.
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.DependencyInjection;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// --- DI Container ---
// Register application services that skill resources and scripts can resolve at execution time.
ServiceCollection services = new();
services.AddSingleton<ConversionService>();
IServiceProvider serviceProvider = services.BuildServiceProvider();
// =====================================================================
// Approach 1: Code-Defined Skill with DI (AgentInlineSkill)
// =====================================================================
// Handles distance conversions (miles ↔ kilometers).
// Resources and scripts are inline delegates. Each delegate can declare
// an IServiceProvider parameter that the framework injects automatically.
var distanceSkill = new AgentInlineSkill(
name: "distance-converter",
description: "Convert between distance units. Use when asked to convert miles to kilometers or kilometers to miles.",
instructions: """
Use this skill when the user asks to convert between distance units (miles and kilometers).
1. Review the distance-table resource to find the factor for the requested conversion.
2. Use the convert script, passing the value and factor from the table.
""")
.AddResource("distance-table", (IServiceProvider serviceProvider) =>
{
var service = serviceProvider.GetRequiredService<ConversionService>();
return service.GetDistanceTable();
})
.AddScript("convert", (double value, double factor, IServiceProvider serviceProvider) =>
{
var service = serviceProvider.GetRequiredService<ConversionService>();
return service.Convert(value, factor);
});
// =====================================================================
// Approach 2: Class-Based Skill with DI (AgentClassSkill)
// =====================================================================
// Handles weight conversions (pounds ↔ kilograms).
// Resources and scripts are encapsulated in a class. Factory methods
// CreateResource and CreateScript accept delegates with IServiceProvider.
//
// Alternatively, class-based skills can accept dependencies through their
// constructor. Register the skill class itself in the ServiceCollection and
// resolve it from the container:
//
// services.AddSingleton<WeightConverterSkill>();
// var weightSkill = serviceProvider.GetRequiredService<WeightConverterSkill>();
var weightSkill = new WeightConverterSkill();
// --- Skills Provider ---
// Both skills are registered with the same provider so the agent can use either one.
var skillsProvider = new AgentSkillsProvider(distanceSkill, weightSkill);
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(
options: new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName,
services: serviceProvider);
// --- Example: Unit conversion ---
// This prompt spans both domains, so the agent will use both skills.
Console.WriteLine("Converting units with DI-powered skills");
Console.WriteLine(new string('-', 60));
AgentResponse response = await agent.RunAsync(
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
Console.WriteLine($"Agent: {response.Text}");
// ---------------------------------------------------------------------------
// Class-Based Skill
// ---------------------------------------------------------------------------
/// <summary>
/// A weight-converter skill defined as a C# class that uses Dependency Injection.
/// </summary>
/// <remarks>
/// This skill resolves <see cref="ConversionService"/> from the DI container
/// in both its resource and script functions. This enables clean separation of
/// concerns and testability while retaining the class-based skill pattern.
/// </remarks>
internal sealed class WeightConverterSkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillResource>? _resources;
private IReadOnlyList<AgentSkillScript>? _scripts;
/// <inheritdoc/>
public override AgentSkillFrontmatter Frontmatter { get; } = new(
"weight-converter",
"Convert between weight units. Use when asked to convert pounds to kilograms or kilograms to pounds.");
/// <inheritdoc/>
protected override string Instructions => """
Use this skill when the user asks to convert between weight units (pounds and kilograms).
1. Review the weight-table resource to find the factor for the requested conversion.
2. Use the convert script, passing the value and factor from the table.
3. Present the result clearly with both units.
""";
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
[
CreateResource("weight-table", (IServiceProvider serviceProvider) =>
{
var service = serviceProvider.GetRequiredService<ConversionService>();
return service.GetWeightTable();
}),
];
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
[
CreateScript("convert", (double value, double factor, IServiceProvider serviceProvider) =>
{
var service = serviceProvider.GetRequiredService<ConversionService>();
return service.Convert(value, factor);
}),
];
}
// ---------------------------------------------------------------------------
// Services
// ---------------------------------------------------------------------------
/// <summary>
/// Provides conversion rates between units.
/// In a real application this could call an external API, read from a database,
/// or apply time-varying exchange rates.
/// </summary>
internal sealed class ConversionService
{
/// <summary>
/// Returns a markdown table of supported distance conversions.
/// </summary>
public string GetDistanceTable() =>
"""
# Distance Conversions
Formula: **result = value × factor**
| From | To | Factor |
|-------------|-------------|----------|
| miles | kilometers | 1.60934 |
| kilometers | miles | 0.621371 |
""";
/// <summary>
/// Returns a markdown table of supported weight conversions.
/// </summary>
public string GetWeightTable() =>
"""
# Weight Conversions
Formula: **result = value × factor**
| From | To | Factor |
|-------------|-------------|----------|
| pounds | kilograms | 0.453592 |
| kilograms | pounds | 2.20462 |
""";
/// <summary>
/// Converts a value by the given factor and returns a JSON result.
/// </summary>
public string Convert(double value, double factor)
{
double result = Math.Round(value * factor, 4);
return JsonSerializer.Serialize(new { value, factor, result });
}
}
@@ -0,0 +1,65 @@
# Agent Skills with Dependency Injection
This sample demonstrates how to use **Dependency Injection (DI)** with Agent Skills. It shows two approaches side-by-side, each handling a different conversion domain:
1. **Code-defined skill** (`AgentInlineSkill`) — converts **distances** (miles ↔ kilometers)
2. **Class-based skill** (`AgentClassSkill`) — converts **weights** (pounds ↔ kilograms)
Both skills resolve the same `ConversionService` from the DI container. When prompted with a question spanning both domains, the agent uses both skills.
## What It Shows
- Registering application services in a `ServiceCollection`
- Defining a **code-defined** skill (distance converter) with resources and scripts that resolve services from `IServiceProvider`
- Defining a **class-based** skill (weight converter) with resources and scripts that resolve services from `IServiceProvider`
- Passing the built `IServiceProvider` to the agent so skills can access DI services at execution time
- Running a single prompt that exercises both skills to show they work together
## How It Works
1. A `ConversionService` is registered as a singleton in the DI container
2. **Code-defined skill**: An `AgentInlineSkill` for distance conversions declares `IServiceProvider` as a parameter in its `AddResource` and `AddScript` delegates — the framework injects it automatically
3. **Class-based skill**: A `WeightConverterSkill` class extends `AgentClassSkill` for weight conversions and uses `CreateResource`/`CreateScript` factory methods with `IServiceProvider` parameters
4. Both skills resolve `ConversionService` from the provider — one for distance tables, the other for weight tables
5. A single agent is created with both skills registered, and the service provider flows through to skill execution
> **Tip:** Class-based skills can also accept dependencies through their **constructor**. Register the skill class in the `ServiceCollection` and resolve it from the container instead of calling `new` directly. This is useful when the skill itself needs injected services beyond what the resource/script delegates use.
## How It Differs from Other Samples
| Sample | Skill Type | DI Support |
|--------|------------|------------|
| [Step02](../Agent_Step02_CodeDefinedSkills/) | Code-defined (`AgentInlineSkill`) | No — static resources |
| [Step03](../Agent_Step03_ClassBasedSkills/) | Class-based (`AgentClassSkill`) | No — static resources |
| **Step05 (this)** | **Both code-defined and class-based** | **Yes — DI via `IServiceProvider`** |
## Prerequisites
- .NET 10
- An Azure OpenAI deployment
## Configuration
Set the following environment variables:
| Variable | Description |
|---|---|
| `AZURE_OPENAI_ENDPOINT` | Your Azure OpenAI endpoint URL |
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Model deployment name (defaults to `gpt-4o-mini`) |
## Running the Sample
```bash
dotnet run
```
### Expected Output
```
Converting units with DI-powered skills
------------------------------------------------------------
Agent: Here are your conversions:
1. **26.2 miles → 42.16 km** (a marathon distance)
2. **75 kg → 165.35 lbs**
```
+23 -10
View File
@@ -6,19 +6,32 @@ Samples demonstrating Agent Skills capabilities. Each sample shows a different w
|--------|-------------|
| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. |
| [Agent_Step02_CodeDefinedSkills](Agent_Step02_CodeDefinedSkills/) | Define skills entirely in C# code using `AgentInlineSkill`, with static/dynamic resources and scripts. |
| [Agent_Step03_ClassBasedSkills](Agent_Step03_ClassBasedSkills/) | Define skills as C# classes using `AgentClassSkill`. |
| [Agent_Step04_MixedSkills](Agent_Step04_MixedSkills/) | **(Advanced)** Combine file-based, code-defined, and class-based skills using `AgentSkillsProviderBuilder`. |
| [Agent_Step05_SkillsWithDI](Agent_Step05_SkillsWithDI/) | Use Dependency Injection with both code-defined (`AgentInlineSkill`) and class-based (`AgentClassSkill`) skills. |
## Key Concepts
### File-Based vs Code-Defined Skills
### Skill Types
| Aspect | File-Based | Code-Defined |
|--------|-----------|--------------|
| Definition | `SKILL.md` files on disk | `AgentInlineSkill` instances in C# |
| Resources | All files in skill directory (filtered by extension) | `AddResource` (static value or delegate-backed) |
| Scripts | Supported via script executor delegate | `AddScript` delegates |
| Discovery | Automatic from directory path | Explicit via constructor |
| Dynamic content | No (static files only) | Yes (factory delegates) |
| Reusability | Copy skill directory | Inline or shared instances |
| Aspect | File-Based | Code-Defined | Class-Based |
|--------|-----------|--------------|-------------|
| Definition | `SKILL.md` files on disk | `AgentInlineSkill` instances in C# | Classes extending `AgentClassSkill` |
| Resources | All files in skill directory (filtered by extension) | `AddResource` (static value or delegate-backed) | `CreateResource` factory methods |
| Scripts | Supported via script runner delegate | `AddScript` delegates | `CreateScript` factory methods |
| Discovery | Automatic from directory path | Explicit via constructor | Explicit via constructor |
| Dynamic content | No (static files only) | Yes (factory delegates) | Yes (factory delegates) |
| Sharing pattern | Copy skill directory | Inline or shared instances | Package in shared assemblies/NuGet |
| DI support | No | Yes (via `IServiceProvider` parameter) | Yes (via `IServiceProvider` parameter) |
For single-source scenarios, use the `AgentSkillsProvider` constructors directly. To combine multiple skill types, use the `AgentSkillsProviderBuilder`.
### `AgentSkillsProvider` vs `AgentSkillsProviderBuilder`
For single-source scenarios, use the `AgentSkillsProvider` constructors directly — they accept a skill directory path, a set of skills, or a custom source.
Use `AgentSkillsProviderBuilder` for advanced scenarios where simple constructors are insufficient:
- **Mixed skill types** — combine file-based, code-defined, and class-based skills in one provider
- **Multiple file script runners** — use different script runners for different file skill directories
- **Skill filtering** — include or exclude skills using a predicate
See [Agent_Step04_MixedSkills](Agent_Step04_MixedSkills/) for a working example.
@@ -44,10 +44,10 @@ ClientResult<VectorStore> vectorStoreCreate = await vectorStoreClient.CreateVect
FileSearchTool fileSearchTool = new([vectorStoreCreate.Value.Id]);
#pragma warning restore OPENAI001
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
"AskContoso",
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: deploymentName)
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(model: deploymentName)
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
Tools = { fileSearchTool }
@@ -68,4 +68,4 @@ Console.WriteLine(await agent.RunAsync("What is the best way to maintain the Tra
// Cleanup
await fileClient.DeleteFileAsync(uploadResult.Value.Id);
await vectorStoreClient.DeleteVectorStoreAsync(vectorStoreCreate.Value.Id);
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
@@ -19,10 +19,10 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYME
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// Create a server side agent and expose it as an AIAgent.
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
"Joker",
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: deploymentName)
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(model: deploymentName)
{
Instructions = "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.",
})
@@ -18,10 +18,10 @@ const string JokerName = "JokerAgent";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Create a server-side agent version using the native SDK.
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
JokerName,
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: deploymentName)
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(model: deploymentName)
{
Instructions = "You are good at telling jokes.",
}));
@@ -33,4 +33,4 @@ FoundryAgent agent = aiProjectClient.AsAIAgent(agentVersion);
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
// Cleanup: deletes the agent and all its versions.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
@@ -7,6 +7,7 @@
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.AI.Projects.Memory;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
@@ -31,10 +31,10 @@ var mcpTool = ResponseTool.CreateMcpTool(
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval));
// Create a server side agent with the mcp tool, and expose it as an AIAgent.
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
"MicrosoftLearnAgent",
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: model)
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(model: model)
{
Instructions = "You answer questions by searching the Microsoft Learn content only.",
Tools = { mcpTool }
@@ -47,7 +47,7 @@ AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", session));
// Cleanup for sample purposes.
aiProjectClient.Agents.DeleteAgent(agent.Name);
aiProjectClient.AgentAdministrationClient.DeleteAgent(agent.Name);
// **** MCP Tool with Approval Required ****
// *****************************************
@@ -61,10 +61,10 @@ var mcpToolWithApproval = ResponseTool.CreateMcpTool(
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval));
// Create an agent with the MCP tool that requires approval.
AgentVersion agentVersionWithApproval = await aiProjectClient.Agents.CreateAgentVersionAsync(
ProjectsAgentVersion agentVersionWithApproval = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
"MicrosoftLearnAgentWithApproval",
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: model)
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(model: model)
{
Instructions = "You answer questions by searching the Microsoft Learn content only.",
Tools = { mcpToolWithApproval }
@@ -58,9 +58,9 @@ public static class Program
finally
{
// Cleanup the agents created for the sample.
await aiProjectClient.Agents.DeleteAgentAsync(frenchAgent.Name);
await aiProjectClient.Agents.DeleteAgentAsync(spanishAgent.Name);
await aiProjectClient.Agents.DeleteAgentAsync(englishAgent.Name);
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(frenchAgent.Name);
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(spanishAgent.Name);
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(englishAgent.Name);
}
}
@@ -76,10 +76,10 @@ public static class Program
AIProjectClient aiProjectClient,
string model)
{
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
$"{targetLanguage} Translator",
new AgentVersionCreationOptions(
new PromptAgentDefinition(model: model)
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(model: model)
{
Instructions = $"You are a translation assistant that translates the provided text to {targetLanguage}.",
}));
@@ -97,7 +97,7 @@ internal sealed class Program
agentDescription: "Escalate agent for human support");
}
private static PromptAgentDefinition DefineSelfServiceAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineSelfServiceAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -144,7 +144,7 @@ internal sealed class Program
}
};
private static PromptAgentDefinition DefineTicketingAgent(IConfiguration configuration, TicketingPlugin plugin) =>
private static DeclarativeAgentDefinition DefineTicketingAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -208,7 +208,7 @@ internal sealed class Program
}
};
private static PromptAgentDefinition DefineTicketRoutingAgent(IConfiguration configuration, TicketingPlugin plugin) =>
private static DeclarativeAgentDefinition DefineTicketRoutingAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -253,7 +253,7 @@ internal sealed class Program
}
};
private static PromptAgentDefinition DefineWindowsSupportAgent(IConfiguration configuration, TicketingPlugin plugin) =>
private static DeclarativeAgentDefinition DefineWindowsSupportAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -323,7 +323,7 @@ internal sealed class Program
}
};
private static PromptAgentDefinition DefineResolutionAgent(IConfiguration configuration, TicketingPlugin plugin) =>
private static DeclarativeAgentDefinition DefineResolutionAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -357,7 +357,7 @@ internal sealed class Program
}
};
private static PromptAgentDefinition TicketEscalationAgent(IConfiguration configuration, TicketingPlugin plugin) =>
private static DeclarativeAgentDefinition TicketEscalationAgent(IConfiguration configuration, TicketingPlugin plugin) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -88,7 +88,7 @@ internal sealed class Program
agentDescription: "Weather agent for DeepResearch workflow");
}
private static PromptAgentDefinition DefineResearchAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineResearchAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -114,13 +114,13 @@ internal sealed class Program
""",
Tools =
{
//AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
//ProjectsAgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
// new BingGroundingSearchToolParameters(
// [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))]))
}
};
private static PromptAgentDefinition DefinePlannerAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefinePlannerAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions = // TODO: Use Structured Inputs / Prompt Template
@@ -139,7 +139,7 @@ internal sealed class Program
"""
};
private static PromptAgentDefinition DefineManagerAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineManagerAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions = // TODO: Use Structured Inputs / Prompt Template
@@ -225,7 +225,7 @@ internal sealed class Program
}
};
private static PromptAgentDefinition DefineSummaryAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineSummaryAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -240,18 +240,18 @@ internal sealed class Program
"""
};
private static PromptAgentDefinition DefineKnowledgeAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineKnowledgeAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Tools =
{
//AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
//ProjectsAgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
// new BingGroundingSearchToolParameters(
// [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))]))
}
};
private static PromptAgentDefinition DefineCoderAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineCoderAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -265,7 +265,7 @@ internal sealed class Program
}
};
private static PromptAgentDefinition DefineWeatherAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineWeatherAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -274,7 +274,7 @@ internal sealed class Program
""",
Tools =
{
AgentTool.CreateOpenApiTool(
ProjectsAgentTool.CreateOpenApiTool(
new OpenApiFunctionDefinition(
"weather-forecast",
BinaryData.FromString(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "wttr.json"))),
@@ -67,9 +67,9 @@ internal sealed class Program
agentDescription: "Provides information about the restaurant menu");
}
private static PromptAgentDefinition DefineMenuAgent(IConfiguration configuration, AIFunction[] functions)
private static DeclarativeAgentDefinition DefineMenuAgent(IConfiguration configuration, AIFunction[] functions)
{
PromptAgentDefinition agentDefinition =
DeclarativeAgentDefinition agentDefinition =
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -46,7 +46,7 @@ internal sealed class Program
await CreateAgentsAsync(aiProjectClient, configuration);
// Ensure workflow agent exists in Foundry.
AgentVersion agentVersion = await CreateWorkflowAsync(aiProjectClient, configuration);
ProjectsAgentVersion agentVersion = await CreateWorkflowAsync(aiProjectClient, configuration);
string workflowInput = GetWorkflowInput(args);
@@ -86,7 +86,7 @@ internal sealed class Program
}
}
private static async Task<AgentVersion> CreateWorkflowAsync(AIProjectClient agentClient, IConfiguration configuration)
private static async Task<ProjectsAgentVersion> CreateWorkflowAsync(AIProjectClient agentClient, IConfiguration configuration)
{
string workflowYaml = File.ReadAllText("MathChat.yaml");
@@ -114,7 +114,7 @@ internal sealed class Program
agentDescription: "Teacher agent for MathChat workflow");
}
private static PromptAgentDefinition DefineStudentAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineStudentAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -127,7 +127,7 @@ internal sealed class Program
"""
};
private static PromptAgentDefinition DefineTeacherAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineTeacherAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -68,7 +68,7 @@ internal sealed class Program
agentDescription: "Chats with the user with location awareness.");
}
private static PromptAgentDefinition DefineLocationTriageAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineLocationTriageAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -79,7 +79,7 @@ internal sealed class Program
"""
};
private static PromptAgentDefinition DefineLocationCaptureAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineLocationCaptureAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -128,7 +128,7 @@ internal sealed class Program
}
};
private static PromptAgentDefinition DefineLocationAwareAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineLocationAwareAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
// Parameterized instructions reference the "location" input argument.
@@ -63,9 +63,9 @@ internal sealed class Program
agentDescription: "Provides information about the restaurant menu");
}
private static PromptAgentDefinition DefineMenuAgent(IConfiguration configuration, AIFunction[] functions)
private static DeclarativeAgentDefinition DefineMenuAgent(IConfiguration configuration, AIFunction[] functions)
{
PromptAgentDefinition agentDefinition =
DeclarativeAgentDefinition agentDefinition =
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -125,9 +125,9 @@ internal sealed class Program
agentDescription: "Provides information based on search results");
}
private static PromptAgentDefinition DefineSearchAgent(IConfiguration configuration)
private static DeclarativeAgentDefinition DefineSearchAgent(IConfiguration configuration)
{
return new PromptAgentDefinition(configuration.GetValue(Application.Settings.FoundryModel))
return new DeclarativeAgentDefinition(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
"""
@@ -67,7 +67,7 @@ internal sealed class Program
agentDescription: "Editor agent for Marketing workflow");
}
private static PromptAgentDefinition DefineAnalystAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineAnalystAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -79,13 +79,13 @@ internal sealed class Program
""",
Tools =
{
//AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
//ProjectsAgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
// new BingGroundingSearchToolParameters(
// [new BingGroundingSearchConfiguration(configuration[Application.Settings.FoundryGroundingTool])]))
}
};
private static PromptAgentDefinition DefineWriterAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineWriterAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -96,7 +96,7 @@ internal sealed class Program
"""
};
private static PromptAgentDefinition DefineEditorAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineEditorAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -62,7 +62,7 @@ internal sealed class Program
agentDescription: "Teacher agent for MathChat workflow");
}
private static PromptAgentDefinition DefineStudentAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineStudentAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -75,7 +75,7 @@ internal sealed class Program
"""
};
private static PromptAgentDefinition DefineTeacherAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineTeacherAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -58,7 +58,7 @@ internal sealed class Program
agentDescription: "Searches documents on Microsoft Learn");
}
private static PromptAgentDefinition DefineSearchAgent(IConfiguration configuration) =>
private static DeclarativeAgentDefinition DefineSearchAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
@@ -6,6 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAIW001</NoWarn> <!-- Handoff Orchestrations are Experimental -->
</PropertyGroup>
<ItemGroup>
@@ -20,7 +20,7 @@ internal static class HostAgentFactory
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
AgentRecord agentRecord = await aiProjectClient.Agents.GetAgentAsync(agentName);
ProjectsAgentRecord agentRecord = await aiProjectClient.AgentAdministrationClient.GetAgentAsync(agentName);
AIAgent agent = aiProjectClient.AsAIAgent(agentRecord, tools: tools);
AgentCard agentCard = agentType.ToUpperInvariant() switch
@@ -3,7 +3,7 @@
<PropertyGroup>
<RootNamespace>Microsoft.Agents.AI</RootNamespace>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
<IsReleaseCandidate>true</IsReleaseCandidate>
<IsReleased>true</IsReleased>
</PropertyGroup>
<PropertyGroup>
@@ -25,8 +25,8 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
{
private readonly ChatClientMetadata? _metadata;
private readonly AIProjectClient _agentClient;
private readonly AgentVersion? _agentVersion;
private readonly AgentRecord? _agentRecord;
private readonly ProjectsAgentVersion? _agentVersion;
private readonly ProjectsAgentRecord? _agentRecord;
private readonly ChatOptions? _chatOptions;
private readonly AgentReference _agentReference;
@@ -56,34 +56,34 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
/// Initializes a new instance of the <see cref="AzureAIProjectChatClient"/> class.
/// </summary>
/// <param name="aiProjectClient">An instance of <see cref="AIProjectClient"/> to interact with Azure AI Agents services.</param>
/// <param name="agentRecord">An instance of <see cref="AgentRecord"/> representing the specific agent to use.</param>
/// <param name="agentRecord">An instance of <see cref="ProjectsAgentRecord"/> representing the specific agent to use.</param>
/// <param name="chatOptions">An instance of <see cref="ChatOptions"/> representing the options on how the agent was predefined.</param>
/// <remarks>
/// The <see cref="IChatClient"/> provided should be decorated with a <see cref="AzureAIProjectChatClient"/> for proper functionality.
/// </remarks>
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentRecord agentRecord, ChatOptions? chatOptions)
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, ProjectsAgentRecord agentRecord, ChatOptions? chatOptions)
: this(aiProjectClient, Throw.IfNull(agentRecord).GetLatestVersion(), chatOptions)
{
this._agentRecord = agentRecord;
}
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentVersion agentVersion, ChatOptions? chatOptions)
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, ProjectsAgentVersion agentVersion, ChatOptions? chatOptions)
: this(
aiProjectClient,
CreateAgentReference(Throw.IfNull(agentVersion)),
(agentVersion.Definition as PromptAgentDefinition)?.Model,
(agentVersion.Definition as DeclarativeAgentDefinition)?.Model,
chatOptions)
{
this._agentVersion = agentVersion;
}
/// <summary>
/// Creates an <see cref="AgentReference"/> from an <see cref="AgentVersion"/>.
/// Creates an <see cref="AgentReference"/> from an <see cref="ProjectsAgentVersion"/>.
/// Uses the agent version's version if available, otherwise defaults to "latest".
/// </summary>
/// <param name="agentVersion">The agent version to create a reference from.</param>
/// <returns>An <see cref="AgentReference"/> for the specified agent version.</returns>
private static AgentReference CreateAgentReference(AgentVersion agentVersion)
private static AgentReference CreateAgentReference(ProjectsAgentVersion agentVersion)
{
// If the version is null, empty, or whitespace, use "latest" as the default.
// This handles cases where hosted agents (like MCP agents) may not have a version assigned.
@@ -98,9 +98,9 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
? this._metadata
: (serviceKey is null && serviceType == typeof(AIProjectClient))
? this._agentClient
: (serviceKey is null && serviceType == typeof(AgentVersion))
: (serviceKey is null && serviceType == typeof(ProjectsAgentVersion))
? this._agentVersion
: (serviceKey is null && serviceType == typeof(AgentRecord))
: (serviceKey is null && serviceType == typeof(ProjectsAgentRecord))
? this._agentRecord
: (serviceKey is null && serviceType == typeof(AgentReference))
? this._agentReference
@@ -38,7 +38,7 @@ public static partial class AzureAIProjectChatClientExtensions
/// <exception cref="InvalidOperationException">The agent with the specified name was not found.</exception>
/// <remarks>
/// When instantiating a <see cref="ChatClientAgent"/> by using an <see cref="AgentReference"/>, minimal information will be available about the agent in the instance level, and any logic that relies
/// on <see cref="AIAgent.GetService{TService}(object?)"/> to retrieve information about the agent like <see cref="AgentVersion" /> will receive <see langword="null"/> as the result.
/// on <see cref="AIAgent.GetService{TService}(object?)"/> to retrieve information about the agent like <see cref="ProjectsAgentVersion" /> will receive <see langword="null"/> as the result.
/// </remarks>
public static FoundryAgent AsAIAgent(
this AIProjectClient aiProjectClient,
@@ -67,7 +67,7 @@ public static partial class AzureAIProjectChatClientExtensions
}
/// <summary>
/// Uses an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AIProjectClient"/> and <see cref="AgentRecord"/>.
/// Uses an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AIProjectClient"/> and <see cref="ProjectsAgentRecord"/>.
/// </summary>
/// <param name="aiProjectClient">The client used to interact with Azure AI Agents. Cannot be <see langword="null"/>.</param>
/// <param name="agentRecord">The agent record to be converted. The latest version will be used. Cannot be <see langword="null"/>.</param>
@@ -78,7 +78,7 @@ public static partial class AzureAIProjectChatClientExtensions
/// <exception cref="ArgumentNullException">Thrown when <paramref name="aiProjectClient"/> or <paramref name="agentRecord"/> is <see langword="null"/>.</exception>
public static FoundryAgent AsAIAgent(
this AIProjectClient aiProjectClient,
AgentRecord agentRecord,
ProjectsAgentRecord agentRecord,
IList<AITool>? tools = null,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null)
@@ -100,7 +100,7 @@ public static partial class AzureAIProjectChatClientExtensions
}
/// <summary>
/// Uses an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AIProjectClient"/> and <see cref="AgentVersion"/>.
/// Uses an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AIProjectClient"/> and <see cref="ProjectsAgentVersion"/>.
/// </summary>
/// <param name="aiProjectClient">The client used to interact with Azure AI Agents. Cannot be <see langword="null"/>.</param>
/// <param name="agentVersion">The agent version to be converted. Cannot be <see langword="null"/>.</param>
@@ -111,7 +111,7 @@ public static partial class AzureAIProjectChatClientExtensions
/// <exception cref="ArgumentNullException">Thrown when <paramref name="aiProjectClient"/> or <paramref name="agentVersion"/> is <see langword="null"/>.</exception>
public static FoundryAgent AsAIAgent(
this AIProjectClient aiProjectClient,
AgentVersion agentVersion,
ProjectsAgentVersion agentVersion,
IList<AITool>? tools = null,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null)
@@ -206,7 +206,7 @@ public static partial class AzureAIProjectChatClientExtensions
/// <summary>Creates a <see cref="ChatClientAgent"/> with the specified options.</summary>
private static ChatClientAgent CreateChatClientAgent(
AIProjectClient aiProjectClient,
AgentVersion agentVersion,
ProjectsAgentVersion agentVersion,
ChatClientAgentOptions agentOptions,
Func<IChatClient, IChatClient>? clientFactory,
IServiceProvider? services)
@@ -249,7 +249,7 @@ public static partial class AzureAIProjectChatClientExtensions
/// <summary>This method creates an <see cref="ChatClientAgent"/> with the specified ChatClientAgentOptions.</summary>
private static ChatClientAgent AsChatClientAgent(
AIProjectClient aiProjectClient,
AgentVersion agentVersion,
ProjectsAgentVersion agentVersion,
ChatClientAgentOptions agentOptions,
Func<IChatClient, IChatClient>? clientFactory,
IServiceProvider? services)
@@ -258,7 +258,7 @@ public static partial class AzureAIProjectChatClientExtensions
/// <summary>This method creates an <see cref="ChatClientAgent"/> with the specified ChatClientAgentOptions.</summary>
private static ChatClientAgent AsChatClientAgent(
AIProjectClient aiProjectClient,
AgentRecord agentRecord,
ProjectsAgentRecord agentRecord,
ChatClientAgentOptions agentOptions,
Func<IChatClient, IChatClient>? clientFactory,
IServiceProvider? services)
@@ -293,14 +293,14 @@ public static partial class AzureAIProjectChatClientExtensions
/// <summary>This method creates an <see cref="ChatClientAgent"/> with a auto-generated ChatClientAgentOptions from the specified configuration parameters.</summary>
private static ChatClientAgent AsChatClientAgent(
AIProjectClient AIProjectClient,
AgentVersion agentVersion,
AIProjectClient aiProjectClient,
ProjectsAgentVersion agentVersion,
IList<AITool>? tools,
Func<IChatClient, IChatClient>? clientFactory,
bool requireInvocableTools,
IServiceProvider? services)
=> AsChatClientAgent(
AIProjectClient,
aiProjectClient,
agentVersion,
CreateChatClientAgentOptions(agentVersion, new ChatOptions() { Tools = tools }, requireInvocableTools),
clientFactory,
@@ -308,21 +308,21 @@ public static partial class AzureAIProjectChatClientExtensions
/// <summary>This method creates an <see cref="ChatClientAgent"/> with a auto-generated ChatClientAgentOptions from the specified configuration parameters.</summary>
private static ChatClientAgent AsChatClientAgent(
AIProjectClient AIProjectClient,
AgentRecord agentRecord,
AIProjectClient aiProjectClient,
ProjectsAgentRecord agentRecord,
IList<AITool>? tools,
Func<IChatClient, IChatClient>? clientFactory,
bool requireInvocableTools,
IServiceProvider? services)
=> AsChatClientAgent(
AIProjectClient,
aiProjectClient,
agentRecord,
CreateChatClientAgentOptions(agentRecord.GetLatestVersion(), new ChatOptions() { Tools = tools }, requireInvocableTools),
clientFactory,
services);
/// <summary>
/// This method creates <see cref="ChatClientAgentOptions"/> for the specified <see cref="AgentVersion"/> and the provided tools.
/// This method creates <see cref="ChatClientAgentOptions"/> for the specified <see cref="ProjectsAgentVersion"/> and the provided tools.
/// </summary>
/// <param name="agentVersion">The agent version.</param>
/// <param name="chatOptions">The <see cref="ChatOptions"/> to use when interacting with the agent.</param>
@@ -334,12 +334,12 @@ public static partial class AzureAIProjectChatClientExtensions
/// This method rebuilds the agent options from the agent definition returned by the version and combine with the in-proc tools when provided
/// this ensures that all required tools are provided and the definition of the agent options are consistent with the agent definition coming from the server.
/// </remarks>
private static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion agentVersion, ChatOptions? chatOptions, bool requireInvocableTools)
private static ChatClientAgentOptions CreateChatClientAgentOptions(ProjectsAgentVersion agentVersion, ChatOptions? chatOptions, bool requireInvocableTools)
{
var agentDefinition = agentVersion.Definition;
List<AITool>? agentTools = null;
if (agentDefinition is PromptAgentDefinition { Tools: { Count: > 0 } definitionTools })
if (agentDefinition is DeclarativeAgentDefinition { Tools: { Count: > 0 } definitionTools })
{
// Check if no tools were provided while the agent definition requires in-proc tools.
if (requireInvocableTools && chatOptions?.Tools is not { Count: > 0 } && definitionTools.Any(t => t is FunctionTool))
@@ -395,7 +395,7 @@ public static partial class AzureAIProjectChatClientExtensions
Description = agentVersion.Description,
};
if (agentDefinition is PromptAgentDefinition promptAgentDefinition)
if (agentDefinition is DeclarativeAgentDefinition promptAgentDefinition)
{
agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new();
agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions;
@@ -17,12 +17,12 @@ namespace Microsoft.Agents.AI.Foundry;
/// </summary>
/// <remarks>
/// <para>
/// This class wraps <see cref="AgentTool"/> (Azure.AI.Projects.OpenAI) and <see cref="ResponseTool"/> (OpenAI SDK) factory methods,
/// This class wraps <see cref="ProjectsAgentTool"/> (Azure.AI.Projects.Agents) and <see cref="ResponseTool"/> (OpenAI SDK) factory methods,
/// returning <see cref="AITool"/> directly — eliminating the need for manual casting and <c>.AsAITool()</c> calls.
/// </para>
/// <para>
/// Instead of writing:
/// <c>((ResponseTool)AgentTool.CreateOpenApiTool(definition)).AsAITool()</c>
/// <c>((ResponseTool)ProjectsAgentTool.CreateOpenApiTool(definition)).AsAITool()</c>
/// You can write:
/// <c>FoundryAITool.CreateOpenApiTool(definition)</c>
/// </para>
@@ -37,7 +37,7 @@ public static class FoundryAITool
/// <returns>An <see cref="AITool"/> wrapping the provided response tool.</returns>
public static AITool FromResponseTool(ResponseTool responseTool) => responseTool.AsAITool();
// --- Azure.AI.Projects.OpenAI AgentTool factories ---
// --- Azure.AI.Projects.OpenAI ProjectsAgentTool factories ---
/// <summary>
/// Creates an <see cref="AITool"/> for OpenAPI tool invocations.
@@ -45,7 +45,7 @@ public static class FoundryAITool
/// <param name="definition">The OpenAPI function definition specifying the API endpoint, schema, and authentication.</param>
/// <returns>An <see cref="AITool"/> that calls the specified OpenAPI endpoint.</returns>
public static AITool CreateOpenApiTool(OpenApiFunctionDefinition definition)
=> ((ResponseTool)AgentTool.CreateOpenApiTool(definition)).AsAITool();
=> ((ResponseTool)ProjectsAgentTool.CreateOpenApiTool(definition)).AsAITool();
/// <summary>
/// Creates an <see cref="AITool"/> for Bing Grounding search.
@@ -53,7 +53,7 @@ public static class FoundryAITool
/// <param name="options">The Bing Grounding search configuration options.</param>
/// <returns>An <see cref="AITool"/> for Bing Grounding search.</returns>
public static AITool CreateBingGroundingTool(BingGroundingSearchToolOptions options)
=> ((ResponseTool)AgentTool.CreateBingGroundingTool(options)).AsAITool();
=> ((ResponseTool)ProjectsAgentTool.CreateBingGroundingTool(options)).AsAITool();
/// <summary>
/// Creates an <see cref="AITool"/> for Bing Custom Search.
@@ -61,7 +61,7 @@ public static class FoundryAITool
/// <param name="parameters">The Bing Custom Search configuration parameters.</param>
/// <returns>An <see cref="AITool"/> for Bing Custom Search.</returns>
public static AITool CreateBingCustomSearchTool(BingCustomSearchToolOptions parameters)
=> ((ResponseTool)AgentTool.CreateBingCustomSearchTool(parameters)).AsAITool();
=> ((ResponseTool)ProjectsAgentTool.CreateBingCustomSearchTool(parameters)).AsAITool();
/// <summary>
/// Creates an <see cref="AITool"/> for Microsoft Fabric data agent.
@@ -69,7 +69,7 @@ public static class FoundryAITool
/// <param name="options">The Fabric data agent configuration options.</param>
/// <returns>An <see cref="AITool"/> for Microsoft Fabric.</returns>
public static AITool CreateMicrosoftFabricTool(FabricDataAgentToolOptions options)
=> ((ResponseTool)AgentTool.CreateMicrosoftFabricTool(options)).AsAITool();
=> ((ResponseTool)ProjectsAgentTool.CreateMicrosoftFabricTool(options)).AsAITool();
/// <summary>
/// Creates an <see cref="AITool"/> for SharePoint grounding.
@@ -77,7 +77,7 @@ public static class FoundryAITool
/// <param name="options">The SharePoint grounding configuration options.</param>
/// <returns>An <see cref="AITool"/> for SharePoint grounding.</returns>
public static AITool CreateSharepointTool(SharePointGroundingToolOptions options)
=> ((ResponseTool)AgentTool.CreateSharepointTool(options)).AsAITool();
=> ((ResponseTool)ProjectsAgentTool.CreateSharepointTool(options)).AsAITool();
/// <summary>
/// Creates an <see cref="AITool"/> for Azure AI Search.
@@ -85,7 +85,7 @@ public static class FoundryAITool
/// <param name="options">Optional Azure AI Search configuration options.</param>
/// <returns>An <see cref="AITool"/> for Azure AI Search.</returns>
public static AITool CreateAzureAISearchTool(AzureAISearchToolOptions? options = null)
=> ((ResponseTool)AgentTool.CreateAzureAISearchTool(options)).AsAITool();
=> ((ResponseTool)ProjectsAgentTool.CreateAzureAISearchTool(options)).AsAITool();
/// <summary>
/// Creates an <see cref="AITool"/> for browser automation.
@@ -93,7 +93,7 @@ public static class FoundryAITool
/// <param name="parameters">The browser automation configuration parameters.</param>
/// <returns>An <see cref="AITool"/> for browser automation.</returns>
public static AITool CreateBrowserAutomationTool(BrowserAutomationToolOptions parameters)
=> ((ResponseTool)AgentTool.CreateBrowserAutomationTool(parameters)).AsAITool();
=> ((ResponseTool)ProjectsAgentTool.CreateBrowserAutomationTool(parameters)).AsAITool();
/// <summary>
/// Creates an <see cref="AITool"/> for structured output capture.
@@ -101,7 +101,7 @@ public static class FoundryAITool
/// <param name="outputs">The structured output definition.</param>
/// <returns>An <see cref="AITool"/> for structured output capture.</returns>
public static AITool CreateStructuredOutputsTool(StructuredOutputDefinition outputs)
=> ((ResponseTool)AgentTool.CreateStructuredOutputsTool(outputs)).AsAITool();
=> ((ResponseTool)ProjectsAgentTool.CreateStructuredOutputsTool(outputs)).AsAITool();
/// <summary>
/// Creates an <see cref="AITool"/> for Agent-to-Agent (A2A) communication.
@@ -110,7 +110,7 @@ public static class FoundryAITool
/// <param name="agentCardPath">Optional path to the agent card.</param>
/// <returns>An <see cref="AITool"/> for A2A communication.</returns>
public static AITool CreateA2ATool(Uri baseUri, string? agentCardPath = null)
=> AgentTool.CreateA2ATool(baseUri, agentCardPath).AsAITool();
=> ProjectsAgentTool.CreateA2ATool(baseUri, agentCardPath).AsAITool();
// --- OpenAI SDK ResponseTool factories ---
@@ -9,6 +9,7 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Projects;
using Azure.AI.Projects.Memory;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Logging;
@@ -4,6 +4,7 @@ using System.ClientModel;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Projects;
using Azure.AI.Projects.Memory;
namespace Microsoft.Agents.AI.Foundry;
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsReleaseCandidate>true</IsReleaseCandidate>
<IsReleased>true</IsReleased>
<InjectSharedThrow>true</InjectSharedThrow>
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
</PropertyGroup>
@@ -2,106 +2,36 @@
<!-- 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:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</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>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</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>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</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>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</Target>
<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>
@@ -1,433 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace OpenAI.Assistants;
/// <summary>
/// Provides extension methods for OpenAI <see cref="AssistantClient"/>
/// to simplify the creation of AI agents that work with OpenAI services.
/// </summary>
/// <remarks>
/// These extensions bridge the gap between OpenAI SDK client objects and the Microsoft Agent Framework,
/// allowing developers to easily create AI agents that leverage OpenAI's chat completion and response services.
/// The methods handle the conversion from OpenAI clients to <see cref="IChatClient"/> instances and then wrap them
/// in <see cref="ChatClientAgent"/> objects that implement the <see cref="AIAgent"/> interface.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIOpenAIAssistants)]
public static class OpenAIAssistantClientExtensions
{
/// <summary>
/// Gets a <see cref="ChatClientAgent"/> from a <see cref="ClientResult{Assistant}"/>.
/// </summary>
/// <param name="assistantClient">The assistant client.</param>
/// <param name="assistantClientResult">The client result containing the assistant.</param>
/// <param name="chatOptions">Optional chat options.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant.</returns>
[Obsolete("The Assistants API has been deprecated. Please use the Responses API instead.")]
public static ChatClientAgent AsAIAgent(
this AssistantClient assistantClient,
ClientResult<Assistant> assistantClientResult,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null)
{
if (assistantClientResult is null)
{
throw new ArgumentNullException(nameof(assistantClientResult));
}
return assistantClient.AsAIAgent(assistantClientResult.Value, chatOptions, clientFactory, services);
}
/// <summary>
/// Gets a <see cref="ChatClientAgent"/> from an <see cref="Assistant"/>.
/// </summary>
/// <param name="assistantClient">The assistant client.</param>
/// <param name="assistantMetadata">The assistant metadata.</param>
/// <param name="chatOptions">Optional chat options.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant.</returns>
[Obsolete("The Assistants API has been deprecated. Please use the Responses API instead.")]
public static ChatClientAgent AsAIAgent(
this AssistantClient assistantClient,
Assistant assistantMetadata,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null)
{
if (assistantMetadata is null)
{
throw new ArgumentNullException(nameof(assistantMetadata));
}
if (assistantClient is null)
{
throw new ArgumentNullException(nameof(assistantClient));
}
var chatClient = assistantClient.AsIChatClient(assistantMetadata.Id);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
if (!string.IsNullOrWhiteSpace(assistantMetadata.Instructions) && chatOptions?.Instructions is null)
{
chatOptions ??= new ChatOptions();
chatOptions.Instructions = assistantMetadata.Instructions;
}
return new ChatClientAgent(chatClient, options: new()
{
Id = assistantMetadata.Id,
Name = assistantMetadata.Name,
Description = assistantMetadata.Description,
ChatOptions = chatOptions
}, services: services);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AssistantClient"/>.
/// </summary>
/// <param name="assistantClient">The <see cref="AssistantClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
/// <param name="agentId"> The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
/// <param name="chatOptions">Options that should apply to all runs of the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant agent.</returns>
[Obsolete("The Assistants API has been deprecated. Please use the Responses API instead.")]
public static async Task<ChatClientAgent> GetAIAgentAsync(
this AssistantClient assistantClient,
string agentId,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (assistantClient is null)
{
throw new ArgumentNullException(nameof(assistantClient));
}
if (string.IsNullOrWhiteSpace(agentId))
{
throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId));
}
var assistantResponse = await assistantClient.GetAssistantAsync(agentId, cancellationToken).ConfigureAwait(false);
return assistantClient.AsAIAgent(assistantResponse, chatOptions, clientFactory, services);
}
/// <summary>
/// Gets a <see cref="ChatClientAgent"/> from a <see cref="ClientResult{Assistant}"/>.
/// </summary>
/// <param name="assistantClient">The assistant client.</param>
/// <param name="assistantClientResult">The client result containing the assistant.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant.</returns>
/// <exception cref="ArgumentNullException"><paramref name="assistantClientResult"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
[Obsolete("The Assistants API has been deprecated. Please use the Responses API instead.")]
public static ChatClientAgent AsAIAgent(
this AssistantClient assistantClient,
ClientResult<Assistant> assistantClientResult,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null)
{
if (assistantClientResult is null)
{
throw new ArgumentNullException(nameof(assistantClientResult));
}
return assistantClient.AsAIAgent(assistantClientResult.Value, options, clientFactory, services);
}
/// <summary>
/// Gets a <see cref="ChatClientAgent"/> from an <see cref="Assistant"/>.
/// </summary>
/// <param name="assistantClient">The assistant client.</param>
/// <param name="assistantMetadata">The assistant metadata.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant.</returns>
/// <exception cref="ArgumentNullException"><paramref name="assistantMetadata"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
[Obsolete("The Assistants API has been deprecated. Please use the Responses API instead.")]
public static ChatClientAgent AsAIAgent(
this AssistantClient assistantClient,
Assistant assistantMetadata,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null)
{
if (assistantMetadata is null)
{
throw new ArgumentNullException(nameof(assistantMetadata));
}
if (assistantClient is null)
{
throw new ArgumentNullException(nameof(assistantClient));
}
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
var chatClient = assistantClient.AsIChatClient(assistantMetadata.Id);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
if (string.IsNullOrWhiteSpace(options.ChatOptions?.Instructions) && !string.IsNullOrWhiteSpace(assistantMetadata.Instructions))
{
options.ChatOptions ??= new ChatOptions();
options.ChatOptions.Instructions = assistantMetadata.Instructions;
}
var mergedOptions = new ChatClientAgentOptions()
{
Id = assistantMetadata.Id,
Name = options.Name ?? assistantMetadata.Name,
Description = options.Description ?? assistantMetadata.Description,
ChatOptions = options.ChatOptions,
AIContextProviders = options.AIContextProviders,
ChatHistoryProvider = options.ChatHistoryProvider,
UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs
};
return new ChatClientAgent(chatClient, mergedOptions, services: services);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AssistantClient"/>.
/// </summary>
/// <param name="assistantClient">The <see cref="AssistantClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
/// <param name="agentId"> The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant agent.</returns>
/// <exception cref="ArgumentNullException"><paramref name="assistantClient"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="agentId"/> is empty or whitespace.</exception>
[Obsolete("The Assistants API has been deprecated. Please use the Responses API instead.")]
public static async Task<ChatClientAgent> GetAIAgentAsync(
this AssistantClient assistantClient,
string agentId,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null,
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (assistantClient is null)
{
throw new ArgumentNullException(nameof(assistantClient));
}
if (string.IsNullOrWhiteSpace(agentId))
{
throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId));
}
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
var assistantResponse = await assistantClient.GetAssistantAsync(agentId, cancellationToken).ConfigureAwait(false);
return assistantClient.AsAIAgent(assistantResponse, options, clientFactory, services);
}
/// <summary>
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
/// </summary>
/// <param name="client">The OpenAI <see cref="AssistantClient" /> to use for the agent.</param>
/// <param name="model">The model identifier to use (e.g., "gpt-4").</param>
/// <param name="instructions">Optional system instructions that define the agent's behavior and personality.</param>
/// <param name="name">Optional name for the agent for identification purposes.</param>
/// <param name="description">Optional description of the agent's capabilities and purpose.</param>
/// <param name="tools">Optional collection of AI tools that the agent can use during conversations.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An <see cref="ChatClientAgent"/> instance backed by the OpenAI Assistant service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="model"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
[Obsolete("The Assistants API has been deprecated. Please use the Responses API instead.")]
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this AssistantClient client,
string model,
string? instructions = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
Func<IChatClient, IChatClient>? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null,
CancellationToken cancellationToken = default) =>
await client.CreateAIAgentAsync(model,
new ChatClientAgentOptions()
{
Name = name,
Description = description,
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
Tools = tools,
Instructions = instructions,
}
},
clientFactory,
loggerFactory,
services,
cancellationToken).ConfigureAwait(false);
/// <summary>
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
/// </summary>
/// <param name="client">The OpenAI <see cref="AssistantClient" /> to use for the agent.</param>
/// <param name="model">The model identifier to use (e.g., "gpt-4").</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An <see cref="ChatClientAgent"/> instance backed by the OpenAI Assistant service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="model"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
[Obsolete("The Assistants API has been deprecated. Please use the Responses API instead.")]
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this AssistantClient client,
string model,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
Throw.IfNull(model);
Throw.IfNull(options);
var assistantOptions = new AssistantCreationOptions()
{
Name = options.Name,
Description = options.Description,
Instructions = options.ChatOptions?.Instructions,
};
// Convert AITools to ToolDefinitions and ToolResources
var toolDefinitionsAndResources = ConvertAIToolsToToolDefinitions(options.ChatOptions?.Tools);
if (toolDefinitionsAndResources.ToolDefinitions is { Count: > 0 } toolDefinitions)
{
toolDefinitions.ForEach(x => assistantOptions.Tools.Add(x));
}
if (toolDefinitionsAndResources.ToolResources is not null)
{
assistantOptions.ToolResources = toolDefinitionsAndResources.ToolResources;
}
// Create the assistant in the assistant service.
var assistantCreateResult = await client.CreateAssistantAsync(model, assistantOptions, cancellationToken).ConfigureAwait(false);
var assistantId = assistantCreateResult.Value.Id;
// Build the local agent object.
var chatClient = client.AsIChatClient(assistantId);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
var agentOptions = options.Clone();
agentOptions.Id = assistantId;
options.ChatOptions ??= new ChatOptions();
options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools;
return new ChatClientAgent(chatClient, agentOptions, loggerFactory, services);
}
private static (List<ToolDefinition>? ToolDefinitions, ToolResources? ToolResources, List<AITool>? FunctionToolsAndOtherTools) ConvertAIToolsToToolDefinitions(IList<AITool>? tools)
{
List<ToolDefinition>? toolDefinitions = null;
ToolResources? toolResources = null;
List<AITool>? functionToolsAndOtherTools = null;
if (tools is not null)
{
foreach (AITool tool in tools)
{
switch (tool)
{
case HostedCodeInterpreterTool codeTool:
toolDefinitions ??= [];
toolDefinitions.Add(new CodeInterpreterToolDefinition());
if (codeTool.Inputs is { Count: > 0 })
{
foreach (var input in codeTool.Inputs)
{
switch (input)
{
case HostedFileContent hostedFile:
// If the input is a HostedFileContent, we can use its ID directly.
toolResources ??= new();
toolResources.CodeInterpreter ??= new();
toolResources.CodeInterpreter.FileIds.Add(hostedFile.FileId);
break;
}
}
}
break;
case HostedFileSearchTool fileSearchTool:
toolDefinitions ??= [];
toolDefinitions.Add(new FileSearchToolDefinition
{
MaxResults = fileSearchTool.MaximumResultCount,
});
if (fileSearchTool.Inputs is { Count: > 0 })
{
foreach (var input in fileSearchTool.Inputs)
{
switch (input)
{
case HostedVectorStoreContent hostedVectorStore:
toolResources ??= new();
toolResources.FileSearch ??= new();
toolResources.FileSearch.VectorStoreIds.Add(hostedVectorStore.VectorStoreId);
break;
}
}
}
break;
default:
functionToolsAndOtherTools ??= [];
functionToolsAndOtherTools.Add(tool);
break;
}
}
}
return (toolDefinitions, toolResources, functionToolsAndOtherTools);
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsReleaseCandidate>true</IsReleaseCandidate>
<IsReleased>true</IsReleased>
<ImplicitUsings>enable</ImplicitUsings>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
@@ -28,7 +28,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative;
/// <param name="projectCredentials">The credentials used to authenticate with the Foundry project. This must be a valid instance of <see cref="TokenCredential"/>.</param>
public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential projectCredentials) : ResponseAgentProvider
{
private readonly Dictionary<string, AgentVersion> _versionCache = [];
private readonly Dictionary<string, ProjectsAgentVersion> _versionCache = [];
private readonly Dictionary<string, AIAgent> _agentCache = [];
private AIProjectClient? _agentClient;
@@ -99,7 +99,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
IDictionary<string, object?>? inputArguments,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
AgentVersion agentVersionResult = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false);
ProjectsAgentVersion agentVersionResult = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false);
AIAgent agent = await this.GetAgentAsync(agentVersionResult, cancellationToken).ConfigureAwait(false);
ChatOptions chatOptions =
@@ -133,10 +133,10 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
}
}
private async Task<AgentVersion> QueryAgentAsync(string agentName, string? agentVersion, CancellationToken cancellationToken = default)
private async Task<ProjectsAgentVersion> QueryAgentAsync(string agentName, string? agentVersion, CancellationToken cancellationToken = default)
{
string agentKey = $"{agentName}:{agentVersion}";
if (this._versionCache.TryGetValue(agentKey, out AgentVersion? targetAgent))
if (this._versionCache.TryGetValue(agentKey, out ProjectsAgentVersion? targetAgent))
{
return targetAgent;
}
@@ -145,8 +145,8 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
if (string.IsNullOrEmpty(agentVersion))
{
AgentRecord agentRecord =
await client.Agents.GetAgentAsync(
ProjectsAgentRecord agentRecord =
await client.AgentAdministrationClient.GetAgentAsync(
agentName,
cancellationToken).ConfigureAwait(false);
@@ -155,7 +155,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
else
{
targetAgent =
await client.Agents.GetAgentVersionAsync(
await client.AgentAdministrationClient.GetAgentVersionAsync(
agentName,
agentVersion,
cancellationToken).ConfigureAwait(false);
@@ -166,7 +166,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
return targetAgent;
}
private async Task<AIAgent> GetAgentAsync(AgentVersion agentVersion, CancellationToken cancellationToken = default)
private async Task<AIAgent> GetAgentAsync(ProjectsAgentVersion agentVersion, CancellationToken cancellationToken = default)
{
if (this._agentCache.TryGetValue(agentVersion.Id, out AIAgent? agent))
{
@@ -29,7 +29,7 @@
</PropertyGroup>
<PropertyGroup>
<IsReleaseCandidate>true</IsReleaseCandidate>
<IsReleased>true</IsReleased>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Specialized;
@@ -154,6 +155,7 @@ public static partial class AgentWorkflowBuilder
/// The <see cref="AIAgent"/> must be capable of understanding those <see cref="AgentRunOptions"/> provided. If the agent
/// ignores the tools or is otherwise unable to advertize them to the underlying provider, handoffs will not occur.
/// </remarks>
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
public static HandoffWorkflowBuilder CreateHandoffBuilderWith(AIAgent initialAgent)
{
Throw.IfNull(initialAgent);
@@ -1,319 +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:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
@@ -23,7 +23,8 @@ internal sealed class StreamingRunEventStream : IRunEventStream
private readonly CancellationTokenSource _runLoopCancellation;
private readonly bool _disableRunLoop;
private Task? _runLoopTask;
private RunStatus _runStatus = RunStatus.NotStarted;
private volatile RunStatus _runStatus = RunStatus.NotStarted;
private int _completionEpoch; // Tracks which completion signal belongs to which consumer iteration
public StreamingRunEventStream(ISuperStepRunner stepRunner, bool disableRunLoop = false)
@@ -127,7 +128,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
// Wait for next input from the consumer
// Works for both Idle (no work) and PendingRequests (waiting for responses)
await this._inputWaiter.WaitForInputAsync(TimeSpan.FromSeconds(1), linkedSource.Token).ConfigureAwait(false);
await this._inputWaiter.WaitForInputAsync(linkedSource.Token).ConfigureAwait(false);
// When signaled, resume running
this._runStatus = RunStatus.Running;
@@ -209,7 +210,10 @@ internal sealed class StreamingRunEventStream : IRunEventStream
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Get the current epoch - we'll only respond to completion signals from this epoch or later
int myEpoch = Volatile.Read(ref this._completionEpoch) + 1;
int currentEpoch = Volatile.Read(ref this._completionEpoch);
bool expectingFreshWork = this._stepRunner.HasUnprocessedMessages || this._runStatus == RunStatus.Running;
int myEpoch = expectingFreshWork ? currentEpoch + 1 : currentEpoch;
// Use custom async enumerable to avoid exceptions on cancellation.
NonThrowingChannelReaderAsyncEnumerable<WorkflowEvent> eventStream = new(this._eventChannel.Reader);
@@ -310,22 +310,40 @@ public abstract class Executor : IIdentified
return result.Result;
}
/// <summary>
/// Invoked once per superstep before any messages are delivered to the Executor.
/// </summary>
/// <param name="context">The workflow context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
protected internal virtual ValueTask OnMessageDeliveryStartingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
/// Invoked once per superstep after all messages have been delivered to the Executor.
/// </summary>
/// <param name="context">The workflow context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
protected internal virtual ValueTask OnMessageDeliveryFinishedAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
/// Invoked before a checkpoint is saved, allowing custom pre-save logic in derived classes.
/// </summary>
/// <param name="context">The workflow context.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
protected internal virtual ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
/// Invoked after a checkpoint is loaded, allowing custom post-load logic in derived classes.
/// </summary>
/// <param name="context">The workflow context.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
protected internal virtual ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Specialized;
using Microsoft.Extensions.AI;
@@ -9,13 +10,21 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
internal static class DiagnosticConstants
{
public const string ExperimentalFeatureDiagnostic = "MAAIW001";
}
/// <inheritdoc/>
[Obsolete("Prefer HandoffWorkflowBuilder (no 's') instead, which has the same API but the preferred name. This will be removed in a future release before GA.")]
#pragma warning disable MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
public sealed class HandoffsWorkflowBuilder(AIAgent initialAgent) : HandoffWorkflowBuilderCore<HandoffsWorkflowBuilder>(initialAgent)
#pragma warning restore MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
{
}
/// <inheritdoc/>
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
public sealed class HandoffWorkflowBuilder(AIAgent initialAgent) : HandoffWorkflowBuilderCore<HandoffWorkflowBuilder>(initialAgent)
{
}
@@ -23,6 +32,7 @@ public sealed class HandoffWorkflowBuilder(AIAgent initialAgent) : HandoffWorkfl
/// <summary>
/// Provides a builder for specifying the handoff relationships between agents and building the resulting workflow.
/// </summary>
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
public class HandoffWorkflowBuilderCore<TBuilder> where TBuilder : HandoffWorkflowBuilderCore<TBuilder>
{
/// <summary>
@@ -182,6 +182,9 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, fromCheckpoint, [], cancellationToken)
.ConfigureAwait(false);
return new(runHandle);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
return run;
}
}
@@ -249,17 +249,33 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
Executor executor = await this.RunContext.EnsureExecutorAsync(receiverId, this.StepTracer, cancellationToken).ConfigureAwait(false);
this.StepTracer.TraceActivated(receiverId);
while (envelopes.TryDequeue(out var envelope))
{
(object message, TypeId messageType) = await TranslateMessageAsync(envelope).ConfigureAwait(false);
await executor.ExecuteCoreAsync(
message,
messageType,
this.RunContext.BindWorkflowContext(receiverId, envelope.TraceContext),
this.TelemetryContext,
cancellationToken
).ConfigureAwait(false);
// TODO: #5084 - Add delivery-level activity (max one per step per executor) to capture non-message
// specific invocations of executor logic.
IWorkflowContext tracelessContext = this.RunContext.BindWorkflowContext(receiverId);
try
{
await executor.OnMessageDeliveryStartingAsync(tracelessContext, cancellationToken)
.ConfigureAwait(false);
while (envelopes.TryDequeue(out var envelope))
{
(object message, TypeId messageType) = await TranslateMessageAsync(envelope).ConfigureAwait(false);
await executor.ExecuteCoreAsync(
message,
messageType,
this.RunContext.BindWorkflowContext(receiverId, envelope.TraceContext),
this.TelemetryContext,
cancellationToken
).ConfigureAwait(false);
}
}
finally
{
await executor.OnMessageDeliveryFinishedAsync(tracelessContext, cancellationToken)
.ConfigureAwait(false);
}
async ValueTask<(object, TypeId)> TranslateMessageAsync(MessageEnvelope envelope)
@@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsReleaseCandidate>true</IsReleaseCandidate>
<IsReleased>true</IsReleased>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
</PropertyGroup>
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Threading;
@@ -31,6 +32,7 @@ internal sealed class HandoffAgentExecutorOptions
public HandoffToolCallFilteringBehavior ToolCallFilteringBehavior { get; set; } = HandoffToolCallFilteringBehavior.HandoffOnly;
}
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
internal sealed class HandoffMessagesFilter
{
private readonly HandoffToolCallFilteringBehavior _filteringBehavior;
@@ -40,6 +42,7 @@ internal sealed class HandoffMessagesFilter
this._filteringBehavior = filteringBehavior;
}
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
internal static bool IsHandoffFunctionName(string name)
{
return name.StartsWith(HandoffWorkflowBuilder.FunctionPrefix, StringComparison.Ordinal);
@@ -164,6 +167,7 @@ internal sealed class HandoffMessagesFilter
}
/// <summary>Executor used to represent an agent in a handoffs workflow, responding to <see cref="HandoffState"/> events.</summary>
[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)]
internal sealed class HandoffAgentExecutor(
AIAgent agent,
HandoffAgentExecutorOptions options) : Executor<HandoffState, HandoffState>(agent.GetDescriptiveId(), declareCrossRunShareable: true), IResettableExecutor
@@ -2,71 +2,176 @@
<!-- 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:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<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.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<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.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<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.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<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.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(System.Collections.Generic.IEnumerable{Microsoft.Agents.AI.AgentInlineSkill},Microsoft.Agents.AI.AgentSkillsProviderOptions,Microsoft.Extensions.Logging.ILoggerFactory)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.ChatClientAgentOptions.get_SimulateServiceStoredChatHistory</Target>
<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>
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsReleaseCandidate>true</IsReleaseCandidate>
<IsReleased>true</IsReleased>
<NoWarn>$(NoWarn);MEAI001;MAAI001</NoWarn>
</PropertyGroup>
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -36,6 +37,11 @@ public abstract class AgentSkillScript
/// </summary>
public string? Description { get; }
/// <summary>
/// Gets the JSON schema describing the parameters accepted by this script, or <see langword="null"/> if not available.
/// </summary>
public virtual JsonElement? ParametersSchema => null;
/// <summary>
/// Runs the script with the given arguments.
/// </summary>
@@ -117,26 +117,24 @@ public sealed partial class AgentSkillsProvider : AIContextProvider
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class
/// with one or more inline (code-defined) skills.
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class.
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
/// </summary>
/// <param name="skills">The inline skills to include.</param>
public AgentSkillsProvider(params AgentInlineSkill[] skills)
: this(skills as IEnumerable<AgentInlineSkill>)
/// <param name="skills">The skills to include.</param>
public AgentSkillsProvider(params AgentSkill[] skills)
: this(skills as IEnumerable<AgentSkill>)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class
/// with inline (code-defined) skills.
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class.
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
/// </summary>
/// <param name="skills">The inline skills to include.</param>
/// <param name="skills">The skills to include.</param>
/// <param name="options">Optional provider configuration.</param>
/// <param name="loggerFactory">Optional logger factory.</param>
public AgentSkillsProvider(
IEnumerable<AgentInlineSkill> skills,
IEnumerable<AgentSkill> skills,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
: this(
@@ -11,15 +11,31 @@ namespace Microsoft.Agents.AI;
/// <summary>
/// Fluent builder for constructing an <see cref="AgentSkillsProvider"/> backed by a composite source.
/// Intended for advanced scenarios where the simple <see cref="AgentSkillsProvider"/> constructors are insufficient.
/// </summary>
/// <remarks>
/// <para>
/// Use this builder to combine multiple skill sources into a single provider:
/// For simple, single-source scenarios, prefer the <see cref="AgentSkillsProvider"/> constructors directly
/// (e.g., passing a skill directory path or a set of skills). Use this builder when you need one or more
/// of the following advanced capabilities:
/// </para>
/// <list type="bullet">
/// <item><description><strong>Mixed skill types</strong> — combine file-based, code-defined (<see cref="AgentInlineSkill"/>),
/// and class-based (<see cref="AgentClassSkill"/>) skills in a single provider.</description></item>
/// <item><description><strong>Multiple file script runners</strong> — use different script runners for different
/// file skill directories via per-source <c>scriptRunner</c> parameters on
/// <see cref="UseFileSkill"/> / <see cref="UseFileSkills(IEnumerable{string}, AgentFileSkillsSourceOptions?, AgentFileSkillScriptRunner?)"/>.</description></item>
/// <item><description><strong>Skill filtering</strong> — include or exclude skills using a predicate
/// via <see cref="UseFilter"/>.</description></item>
/// </list>
/// <para>
/// Example — combining file-based and code-defined skills:
/// </para>
/// <code>
/// var provider = new AgentSkillsProviderBuilder()
/// .UseFileSkills("/path/to/skills")
/// .UseSkills(myInlineSkill1, myInlineSkill2)
/// .UseFileScriptRunner(SubprocessScriptRunner.RunAsync)
/// .Build();
/// </code>
/// </remarks>
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Abstract base class for defining skills as C# classes that bundle all components together.
/// </summary>
/// <remarks>
/// <para>
/// Inherit from this class to create a self-contained skill definition. Override the abstract
/// properties to provide name, description, and instructions. Use <see cref="CreateResource(string, object, string?)"/>,
/// <see cref="CreateResource(string, Delegate, string?)"/>, and <see cref="CreateScript"/> to define
/// inline resources and scripts.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// public class PdfFormatterSkill : AgentClassSkill
/// {
/// private IReadOnlyList&lt;AgentSkillResource&gt;? _resources;
/// private IReadOnlyList&lt;AgentSkillScript&gt;? _scripts;
///
/// public override AgentSkillFrontmatter Frontmatter { get; } = new("pdf-formatter", "Format documents as PDF.");
/// protected override string Instructions =&gt; "Use this skill to format documents...";
///
/// public override IReadOnlyList&lt;AgentSkillResource&gt;? Resources =&gt; this._resources ??=
/// [
/// CreateResource("template", "Use this template..."),
/// ];
///
/// public override IReadOnlyList&lt;AgentSkillScript&gt;? Scripts =&gt; this._scripts ??=
/// [
/// CreateScript("format-pdf", FormatPdf),
/// ];
///
/// private static string FormatPdf(string content) =&gt; content;
/// }
/// </code>
/// </example>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public abstract class AgentClassSkill : AgentSkill
{
private string? _content;
/// <summary>
/// Gets the raw instructions text for this skill.
/// </summary>
protected abstract string Instructions { get; }
/// <inheritdoc/>
/// <remarks>
/// Returns a synthesized XML document containing name, description, instructions, resources, and scripts.
/// The result is cached after the first access. Override to provide custom content.
/// </remarks>
public override string Content => this._content ??= AgentInlineSkillContentBuilder.Build(
this.Frontmatter.Name,
this.Frontmatter.Description,
this.Instructions,
this.Resources,
this.Scripts);
/// <summary>
/// Creates a skill resource backed by a static value.
/// </summary>
/// <param name="name">The resource name.</param>
/// <param name="value">The static resource value.</param>
/// <param name="description">An optional description of the resource.</param>
/// <returns>A new <see cref="AgentSkillResource"/> instance.</returns>
protected static AgentSkillResource CreateResource(string name, object value, string? description = null)
=> new AgentInlineSkillResource(name, value, description);
/// <summary>
/// Creates a skill resource backed by a delegate that produces a dynamic value.
/// </summary>
/// <param name="name">The resource name.</param>
/// <param name="method">A method that produces the resource value when requested.</param>
/// <param name="description">An optional description of the resource.</param>
/// <returns>A new <see cref="AgentSkillResource"/> instance.</returns>
protected static AgentSkillResource CreateResource(string name, Delegate method, string? description = null)
=> new AgentInlineSkillResource(name, method, description);
/// <summary>
/// Creates a skill script backed by a delegate.
/// </summary>
/// <param name="name">The script name.</param>
/// <param name="method">A method to execute when the script is invoked.</param>
/// <param name="description">An optional description of the script.</param>
/// <returns>A new <see cref="AgentSkillScript"/> instance.</returns>
protected static AgentSkillScript CreateScript(string name, Delegate method, string? description = null)
=> new AgentInlineSkillScript(name, method, description);
}
@@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
@@ -27,8 +25,8 @@ namespace Microsoft.Agents.AI;
public sealed class AgentInlineSkill : AgentSkill
{
private readonly string _instructions;
private List<AgentSkillResource>? _resources;
private List<AgentSkillScript>? _scripts;
private List<AgentInlineSkillResource>? _resources;
private List<AgentInlineSkillScript>? _scripts;
private string? _cachedContent;
/// <summary>
@@ -77,7 +75,7 @@ public sealed class AgentInlineSkill : AgentSkill
public override AgentSkillFrontmatter Frontmatter { get; }
/// <inheritdoc/>
public override string Content => this._cachedContent ??= this.BuildContent();
public override string Content => this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._resources, this._scripts);
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources;
@@ -125,91 +123,4 @@ public sealed class AgentInlineSkill : AgentSkill
(this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description));
return this;
}
private string BuildContent()
{
var sb = new StringBuilder();
sb.Append($"<name>{EscapeXmlString(this.Frontmatter.Name)}</name>\n")
.Append($"<description>{EscapeXmlString(this.Frontmatter.Description)}</description>\n\n")
.Append("<instructions>\n")
.Append(EscapeXmlString(this._instructions))
.Append("\n</instructions>");
if (this.Resources is { Count: > 0 })
{
sb.Append("\n\n<resources>\n");
foreach (var resource in this.Resources)
{
if (resource.Description is not null)
{
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\" description=\"{EscapeXmlString(resource.Description)}\"/>\n");
}
else
{
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\"/>\n");
}
}
sb.Append("</resources>");
}
if (this.Scripts is { Count: > 0 })
{
sb.Append("\n\n<scripts>\n");
foreach (var script in this.Scripts)
{
JsonElement? parametersSchema = ((AgentInlineSkillScript)script).ParametersSchema;
if (script.Description is null && parametersSchema is null)
{
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
}
else
{
sb.Append(script.Description is not null
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");
if (parametersSchema is not null)
{
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
}
sb.Append(" </script>\n");
}
}
sb.Append("</scripts>");
}
return sb.ToString();
}
/// <summary>
/// Escapes XML special characters: always escapes <c>&amp;</c>, <c>&lt;</c>, <c>&gt;</c>,
/// <c>&quot;</c>, and <c>&apos;</c>. When <paramref name="preserveQuotes"/> is <see langword="true"/>,
/// quotes are left unescaped to preserve readability of embedded content such as JSON.
/// </summary>
/// <param name="value">The string to escape.</param>
/// <param name="preserveQuotes">
/// When <see langword="true"/>, leaves <c>"</c> and <c>'</c> unescaped for use in XML element content (e.g., JSON).
/// When <see langword="false"/> (default), escapes all XML special characters including quotes.
/// </param>
private static string EscapeXmlString(string value, bool preserveQuotes = false)
{
var result = value
.Replace("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;");
if (!preserveQuotes)
{
result = result
.Replace("\"", "&quot;")
.Replace("'", "&apos;");
}
return result;
}
}
@@ -0,0 +1,118 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Internal helper that builds XML-structured content strings for code-defined and class-based skills.
/// </summary>
internal static class AgentInlineSkillContentBuilder
{
/// <summary>
/// Builds the complete skill content containing name, description, instructions, resources, and scripts.
/// </summary>
/// <param name="name">The skill name.</param>
/// <param name="description">The skill description.</param>
/// <param name="instructions">The raw instructions text.</param>
/// <param name="resources">Optional resources associated with the skill.</param>
/// <param name="scripts">Optional scripts associated with the skill.</param>
/// <returns>An XML-structured content string.</returns>
public static string Build(
string name,
string description,
string instructions,
IReadOnlyList<AgentSkillResource>? resources,
IReadOnlyList<AgentSkillScript>? scripts)
{
_ = Throw.IfNullOrWhitespace(name);
_ = Throw.IfNullOrWhitespace(description);
_ = Throw.IfNullOrWhitespace(instructions);
var sb = new StringBuilder();
sb.Append($"<name>{EscapeXmlString(name)}</name>\n")
.Append($"<description>{EscapeXmlString(description)}</description>\n\n")
.Append("<instructions>\n")
.Append(EscapeXmlString(instructions))
.Append("\n</instructions>");
if (resources is { Count: > 0 })
{
sb.Append("\n\n<resources>\n");
foreach (var resource in resources)
{
if (resource.Description is not null)
{
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\" description=\"{EscapeXmlString(resource.Description)}\"/>\n");
}
else
{
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\"/>\n");
}
}
sb.Append("</resources>");
}
if (scripts is { Count: > 0 })
{
sb.Append("\n\n<scripts>\n");
foreach (var script in scripts)
{
var parametersSchema = script.ParametersSchema;
if (script.Description is null && parametersSchema is null)
{
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
}
else
{
sb.Append(script.Description is not null
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");
if (parametersSchema is not null)
{
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
}
sb.Append(" </script>\n");
}
}
sb.Append("</scripts>");
}
return sb.ToString();
}
/// <summary>
/// Escapes XML special characters: always escapes <c>&amp;</c>, <c>&lt;</c>, <c>&gt;</c>,
/// <c>&quot;</c>, and <c>&apos;</c>. When <paramref name="preserveQuotes"/> is <see langword="true"/>,
/// quotes are left unescaped to preserve readability of embedded content such as JSON.
/// </summary>
/// <param name="value">The string to escape.</param>
/// <param name="preserveQuotes">
/// When <see langword="true"/>, leaves <c>"</c> and <c>'</c> unescaped for use in XML element content (e.g., JSON).
/// When <see langword="false"/> (default), escapes all XML special characters including quotes.
/// </param>
private static string EscapeXmlString(string value, bool preserveQuotes = false)
{
var result = value
.Replace("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;");
if (!preserveQuotes)
{
result = result
.Replace("\"", "&quot;")
.Replace("'", "&apos;");
}
return result;
}
}
@@ -36,7 +36,7 @@ internal sealed class AgentInlineSkillScript : AgentSkillScript
/// <summary>
/// Gets the JSON schema describing the parameters accepted by this script, or <see langword="null"/> if not available.
/// </summary>
public JsonElement? ParametersSchema => this._function.JsonSchema;
public override JsonElement? ParametersSchema => this._function.JsonSchema;
/// <inheritdoc/>
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default)
@@ -26,7 +26,6 @@ internal static class DiagnosticIds
// We use the same IDs so consumers do not need to suppress additional diagnostics
// when using the experimental OpenAI APIs.
internal const string AIOpenAIResponses = "OPENAI001";
internal const string AIOpenAIAssistants = "OPENAI001";
private const string MEAIExperiments = "MEAI001";
}
@@ -12,13 +12,13 @@ namespace Shared.Foundry;
internal static class AgentFactory
{
public static async ValueTask<AgentVersion> CreateAgentAsync(
public static async ValueTask<ProjectsAgentVersion> CreateAgentAsync(
this AIProjectClient aiProjectClient,
string agentName,
AgentDefinition agentDefinition,
ProjectsAgentDefinition agentDefinition,
string agentDescription)
{
AgentVersionCreationOptions options =
ProjectsAgentVersionCreationOptions options =
new(agentDefinition)
{
Description = agentDescription,
@@ -29,7 +29,7 @@ internal static class AgentFactory
},
};
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(agentName, options).ConfigureAwait(false);
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(agentName, options).ConfigureAwait(false);
Console.ForegroundColor = ConsoleColor.Cyan;
try
@@ -17,7 +17,7 @@ namespace Foundry.IntegrationTests;
/// <summary>
/// Integration tests for versioned <see cref="FoundryAgent"/> creation via
/// <c>AIProjectClient.Agents.CreateAgentVersionAsync</c> and <c>AIProjectClient.AsAIAgent(AgentVersion)</c>.
/// <c>AIProjectClient.AgentAdministrationClient.CreateAgentVersionAsync</c> and <c>AIProjectClient.AsAIAgent(ProjectsAgentVersion)</c>.
/// </summary>
public class FoundryVersionedAgentCreateTests
{
@@ -32,10 +32,10 @@ public class FoundryVersionedAgentCreateTests
const string AgentInstructions = "You are an integration test agent";
// Act.
var agentVersion = await this._client.Agents.CreateAgentVersionAsync(
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
AgentName,
new AgentVersionCreationOptions(
new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = AgentInstructions
})
@@ -53,17 +53,17 @@ public class FoundryVersionedAgentCreateTests
Assert.Equal(AgentDescription, agent.Description);
Assert.Equal(AgentInstructions, agent.GetService<ChatClientAgent>()!.Instructions);
var agentRecord = await this._client.Agents.GetAgentAsync(agent.Name);
var agentRecord = await this._client.AgentAdministrationClient.GetAgentAsync(agent.Name);
Assert.NotNull(agentRecord);
Assert.Equal(AgentName, agentRecord.Value.Name);
var definition = Assert.IsType<PromptAgentDefinition>(agentRecord.Value.GetLatestVersion().Definition);
var definition = Assert.IsType<DeclarativeAgentDefinition>(agentRecord.Value.GetLatestVersion().Definition);
Assert.Equal(AgentDescription, agentRecord.Value.GetLatestVersion().Description);
Assert.Equal(AgentInstructions, definition.Instructions);
}
finally
{
// Cleanup.
await this._client.Agents.DeleteAgentAsync(agent.Name);
await this._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
}
}
@@ -95,15 +95,15 @@ public class FoundryVersionedAgentCreateTests
var vectorStoreMetadata = await projectOpenAIClient.GetProjectVectorStoresClient().CreateVectorStoreAsync(options: new() { FileIds = { uploadedAgentFile.Id }, Name = "WordCodeLookup_VectorStore" });
// Act — create agent version with FileSearch tool via native SDK, then wrap with AsAIAgent.
var definition = new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = AgentInstructions,
Tools = { ResponseTool.CreateFileSearchTool(vectorStoreIds: [vectorStoreMetadata.Value.Id]) }
};
var agentVersion = await this._client.Agents.CreateAgentVersionAsync(
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
AgentName,
new AgentVersionCreationOptions(definition));
new ProjectsAgentVersionCreationOptions(definition));
var agent = this._client.AsAIAgent(agentVersion);
@@ -117,7 +117,7 @@ public class FoundryVersionedAgentCreateTests
finally
{
// Cleanup.
await this._client.Agents.DeleteAgentAsync(agent.Name);
await this._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
await projectOpenAIClient.GetProjectVectorStoresClient().DeleteVectorStoreAsync(vectorStoreMetadata.Value.Id);
await projectOpenAIClient.GetProjectFilesClient().DeleteFileAsync(uploadedAgentFile.Id);
File.Delete(searchFilePath);
@@ -149,15 +149,15 @@ public class FoundryVersionedAgentCreateTests
);
// Act — create agent version with CodeInterpreter tool via native SDK, then wrap with AsAIAgent.
var definition = new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = AgentInstructions,
Tools = { ResponseTool.CreateCodeInterpreterTool(new CodeInterpreterToolContainer(CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration([uploadedCodeFile.Id]))) }
};
var agentVersion = await this._client.Agents.CreateAgentVersionAsync(
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
AgentName,
new AgentVersionCreationOptions(definition));
new ProjectsAgentVersionCreationOptions(definition));
var agent = this._client.AsAIAgent(agentVersion);
@@ -171,7 +171,7 @@ public class FoundryVersionedAgentCreateTests
finally
{
// Cleanup.
await this._client.Agents.DeleteAgentAsync(agent.Name);
await this._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
await projectOpenAIClient.GetProjectFilesClient().DeleteFileAsync(uploadedCodeFile.Id);
File.Delete(codeFilePath);
}
@@ -252,14 +252,14 @@ public class FoundryVersionedAgentCreateTests
Description = "Retrieve information about countries by currency code"
};
var definition = new PromptAgentDefinition(model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
var definition = new DeclarativeAgentDefinition(model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = AgentInstructions,
Tools = { (ResponseTool)AgentTool.CreateOpenApiTool(openApiFunction) }
Tools = { (ResponseTool)ProjectsAgentTool.CreateOpenApiTool(openApiFunction) }
};
AgentVersionCreationOptions creationOptions = new(definition);
AgentVersion agentVersion = await this._client.Agents.CreateAgentVersionAsync(AgentName, creationOptions);
ProjectsAgentVersionCreationOptions creationOptions = new(definition);
ProjectsAgentVersion agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(AgentName, creationOptions);
try
{
@@ -269,7 +269,7 @@ public class FoundryVersionedAgentCreateTests
// Assert the agent was created correctly and retains version metadata.
Assert.NotNull(agent);
Assert.Equal(AgentName, agent.Name);
var retrievedVersion = agent.GetService<AgentVersion>();
var retrievedVersion = agent.GetService<ProjectsAgentVersion>();
Assert.NotNull(retrievedVersion);
// Step 3: Call RunAsync to trigger the server-side OpenAPI function.
@@ -301,7 +301,7 @@ public class FoundryVersionedAgentCreateTests
finally
{
// Cleanup.
await this._client.Agents.DeleteAgentAsync(AgentName);
await this._client.AgentAdministrationClient.DeleteAgentAsync(AgentName);
}
}
@@ -317,15 +317,15 @@ public class FoundryVersionedAgentCreateTests
// Create agent version with the function tool registered in the server-side definition,
// then wrap with AsAIAgent passing the local AIFunction implementation.
var definition = new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = AgentInstructions,
};
definition.Tools.Add(weatherFunction.AsOpenAIResponseTool());
var agentVersion = await this._client.Agents.CreateAgentVersionAsync(
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
AgentName,
new AgentVersionCreationOptions(definition));
new ProjectsAgentVersionCreationOptions(definition));
FoundryAgent agent = this._client.AsAIAgent(agentVersion, tools: [weatherFunction]);
@@ -342,7 +342,7 @@ public class FoundryVersionedAgentCreateTests
}
finally
{
await this._client.Agents.DeleteAgentAsync(agent.Name);
await this._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
}
}
}
@@ -19,8 +19,8 @@ namespace Foundry.IntegrationTests;
/// <summary>
/// Integration test fixture that creates versioned Foundry agents via
/// <c>AIProjectClient.Agents.CreateAgentVersionAsync</c> and wraps them
/// with <c>AIProjectClient.AsAIAgent(AgentVersion)</c>.
/// <c>AIProjectClient.AgentAdministrationClient.CreateAgentVersionAsync</c> and wraps them
/// with <c>AIProjectClient.AsAIAgent(ProjectsAgentVersion)</c>.
/// </summary>
public class FoundryVersionedAgentFixture : IChatClientAgentFixture
{
@@ -121,7 +121,7 @@ public class FoundryVersionedAgentFixture : IChatClientAgentFixture
string instructions = "You are a helpful assistant.",
IList<AITool>? aiTools = null)
{
var definition = new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = instructions
};
@@ -139,9 +139,9 @@ public class FoundryVersionedAgentFixture : IChatClientAgentFixture
}
}
var agentVersion = await this._client.Agents.CreateAgentVersionAsync(
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
GenerateUniqueAgentName(name),
new AgentVersionCreationOptions(definition));
new ProjectsAgentVersionCreationOptions(definition));
return this._client.AsAIAgent(agentVersion, tools: aiTools).GetService<ChatClientAgent>()!;
}
@@ -150,15 +150,15 @@ public class FoundryVersionedAgentFixture : IChatClientAgentFixture
{
options.Name ??= GenerateUniqueAgentName("HelpfulAssistant");
var definition = new PromptAgentDefinition(
var definition = new DeclarativeAgentDefinition(
options.ChatOptions?.ModelId ?? TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = options.ChatOptions?.Instructions
};
var agentVersion = await this._client.Agents.CreateAgentVersionAsync(
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
options.Name,
new AgentVersionCreationOptions(definition) { Description = options.Description });
new ProjectsAgentVersionCreationOptions(definition) { Description = options.Description });
var agent = this._client.AsAIAgent(agentVersion, tools: options.ChatOptions?.Tools);
@@ -169,7 +169,7 @@ public class FoundryVersionedAgentFixture : IChatClientAgentFixture
$"{baseName}-{Guid.NewGuid().ToString("N").Substring(0, 8)}";
public Task DeleteAgentAsync(ChatClientAgent agent) =>
this._client.Agents.DeleteAgentAsync(agent.Name);
this._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
public async Task DeleteSessionAsync(AgentSession session)
{
@@ -201,7 +201,7 @@ public class FoundryVersionedAgentFixture : IChatClientAgentFixture
if (this._client is not null && this._agent is not null)
{
return new ValueTask(this._client.Agents.DeleteAgentAsync(this._agent.Name));
return new ValueTask(this._client.AgentAdministrationClient.DeleteAgentAsync(this._agent.Name));
}
return default;
@@ -211,10 +211,10 @@ public class FoundryVersionedAgentFixture : IChatClientAgentFixture
{
this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential());
var agentVersion = await this._client.Agents.CreateAgentVersionAsync(
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
GenerateUniqueAgentName("HelpfulAssistant"),
new AgentVersionCreationOptions(
new PromptAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = "You are a helpful assistant."
}));
@@ -227,15 +227,15 @@ public class FoundryVersionedAgentFixture : IChatClientAgentFixture
this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential());
options.Name ??= GenerateUniqueAgentName("HelpfulAssistant");
var definition = new PromptAgentDefinition(
var definition = new DeclarativeAgentDefinition(
options.ChatOptions?.ModelId ?? TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = options.ChatOptions?.Instructions
};
var agentVersion = await this._client.Agents.CreateAgentVersionAsync(
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
options.Name,
new AgentVersionCreationOptions(definition) { Description = options.Description });
new ProjectsAgentVersionCreationOptions(definition) { Description = options.Description });
this._agent = this._client.AsAIAgent(agentVersion, tools: options.ChatOptions?.Tools);
}
@@ -3,6 +3,7 @@
using System;
using System.Threading.Tasks;
using Azure.AI.Projects;
using Azure.AI.Projects.Memory;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
@@ -187,7 +187,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#endregion
#region AsAIAgent(AIProjectClient, AgentRecord) Tests
#region AsAIAgent(AIProjectClient, ProjectsAgentRecord) Tests
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
@@ -197,7 +197,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient? client = null;
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
@@ -217,20 +217,20 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
mockClient.Object.AsAIAgent((AgentRecord)null!));
mockClient.Object.AsAIAgent((ProjectsAgentRecord)null!));
Assert.Equal("agentRecord", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord creates a valid agent.
/// Verify that AsAIAgent with ProjectsAgentRecord creates a valid agent.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecord_CreatesValidAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
@@ -243,14 +243,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord and clientFactory applies the factory.
/// Verify that AsAIAgent with ProjectsAgentRecord and clientFactory applies the factory.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecord_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
TestChatClient? testChatClient = null;
// Act
@@ -267,7 +267,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#endregion
#region AsAIAgent(AIProjectClient, AgentVersion) Tests
#region AsAIAgent(AIProjectClient, ProjectsAgentVersion) Tests
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
@@ -277,7 +277,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient? client = null;
AgentVersion agentVersion = this.CreateTestAgentVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
@@ -297,20 +297,20 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
mockClient.Object.AsAIAgent((AgentVersion)null!));
mockClient.Object.AsAIAgent((ProjectsAgentVersion)null!));
Assert.Equal("agentVersion", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion creates a valid agent.
/// Verify that AsAIAgent with ProjectsAgentVersion creates a valid agent.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersion_CreatesValidAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
@@ -323,14 +323,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion and clientFactory applies the factory.
/// Verify that AsAIAgent with ProjectsAgentVersion and clientFactory applies the factory.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersion_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
TestChatClient? testChatClient = null;
// Act
@@ -353,7 +353,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
@@ -375,7 +375,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act - should not throw even without tools when requireInvocableTools is false
var agent = client.AsAIAgent(agentVersion);
@@ -439,7 +439,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#endregion
#region AsAIAgent(AIProjectClient, AgentRecord) with tools Tests
#region AsAIAgent(AIProjectClient, ProjectsAgentRecord) with tools Tests
/// <summary>
/// Verify that AsAIAgent with additional tools when the definition has no tools does not throw and results in an agent with no tools.
@@ -449,7 +449,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
@@ -463,9 +463,9 @@ public sealed class AzureAIProjectChatClientExtensionsTests
Assert.IsType<FoundryAgent>(agent);
var chatClient = agent.GetService<IChatClient>();
Assert.NotNull(chatClient);
var agentVersion = chatClient.GetService<AgentVersion>();
var agentVersion = chatClient.GetService<ProjectsAgentVersion>();
Assert.NotNull(agentVersion);
var definition = Assert.IsType<PromptAgentDefinition>(agentVersion.Definition);
var definition = Assert.IsType<DeclarativeAgentDefinition>(agentVersion.Definition);
Assert.Empty(definition.Tools);
}
@@ -477,7 +477,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord, tools: null);
@@ -502,7 +502,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var agentVersion = this.CreateTestAgentVersion();
// Manually add tools to the definition to simulate inline tools
if (agentVersion.Definition is PromptAgentDefinition promptDef)
if (agentVersion.Definition is DeclarativeAgentDefinition promptDef)
{
promptDef.Tools.Add(ResponseTool.CreateFunctionTool("inline_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
}
@@ -513,9 +513,9 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Act & Assert
var agent = client.AsAIAgent(agentVersion, tools: [invocableInlineAITool, shouldBeIgnoredTool]);
Assert.NotNull(agent);
var version = agent.GetService<AgentVersion>();
var version = agent.GetService<ProjectsAgentVersion>();
Assert.NotNull(version);
var definition = Assert.IsType<PromptAgentDefinition>(version.Definition);
var definition = Assert.IsType<DeclarativeAgentDefinition>(version.Definition);
Assert.NotEmpty(definition.Tools);
Assert.NotNull(GetAgentChatOptions(agent));
Assert.NotNull(GetAgentChatOptions(agent)!.Tools);
@@ -535,7 +535,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "tool1", "param_tool_1", "First parameter tool"),
@@ -550,7 +550,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
Assert.IsType<FoundryAgent>(agent);
var chatClient = agent.GetService<IChatClient>();
Assert.NotNull(chatClient);
var agentVersion = chatClient.GetService<AgentVersion>();
var agentVersion = chatClient.GetService<ProjectsAgentVersion>();
Assert.NotNull(agentVersion);
}
@@ -565,7 +565,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task CreateAIAgentAsync_WithResponseToolsInDefinition_CreatesAgentSuccessfullyAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
var definition = new DeclarativeAgentDefinition("test-model") { Instructions = "Test instructions" };
var fabricToolOptions = new FabricDataAgentToolOptions();
fabricToolOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id"));
@@ -577,33 +577,33 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Add tools to the definition
definition.Tools.Add(ResponseTool.CreateFunctionTool("create_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
definition.Tools.Add((ResponseTool)AgentTool.CreateBingCustomSearchTool(new BingCustomSearchToolOptions([new BingCustomSearchConfiguration("connection-id", "instance-name")])));
definition.Tools.Add((ResponseTool)AgentTool.CreateBrowserAutomationTool(new BrowserAutomationToolOptions(new BrowserAutomationToolConnectionParameters("id"))));
definition.Tools.Add(AgentTool.CreateA2ATool(new Uri("https://test-uri.microsoft.com")));
definition.Tools.Add((ResponseTool)AgentTool.CreateBingGroundingTool(new BingGroundingSearchToolOptions([new BingGroundingSearchConfiguration("connection-id")])));
definition.Tools.Add((ResponseTool)AgentTool.CreateMicrosoftFabricTool(fabricToolOptions));
definition.Tools.Add((ResponseTool)AgentTool.CreateOpenApiTool(new OpenApiFunctionDefinition("name", BinaryData.FromString(OpenAPISpec), new OpenAPIAnonymousAuthenticationDetails())));
definition.Tools.Add((ResponseTool)AgentTool.CreateSharepointTool(sharepointOptions));
definition.Tools.Add((ResponseTool)AgentTool.CreateStructuredOutputsTool(structuredOutputs));
definition.Tools.Add((ResponseTool)AgentTool.CreateAzureAISearchTool(new AzureAISearchToolOptions([new AzureAISearchToolIndex() { IndexName = "name" }])));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateBingCustomSearchTool(new BingCustomSearchToolOptions([new BingCustomSearchConfiguration("connection-id", "instance-name")])));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateBrowserAutomationTool(new BrowserAutomationToolOptions(new BrowserAutomationToolConnectionParameters("id"))));
definition.Tools.Add(ProjectsAgentTool.CreateA2ATool(new Uri("https://test-uri.microsoft.com")));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateBingGroundingTool(new BingGroundingSearchToolOptions([new BingGroundingSearchConfiguration("connection-id")])));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateMicrosoftFabricTool(fabricToolOptions));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateOpenApiTool(new OpenApiFunctionDefinition("name", BinaryData.FromString(OpenAPISpec), new OpenAPIAnonymousAuthenticationDetails())));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateSharepointTool(sharepointOptions));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateStructuredOutputsTool(structuredOutputs));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateAzureAISearchTool(new AzureAISearchToolOptions([new AzureAISearchToolIndex() { IndexName = "name" }])));
// Generate agent definition response with the tools
var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList());
using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
var options = new ProjectsAgentVersionCreationOptions(definition);
// Act
var agentVersion = (await testClient.Client.Agents.CreateAgentVersionAsync("test-agent", options)).Value;
var agentVersion = (await testClient.Client.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", options)).Value;
var agent = testClient.Client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType<FoundryAgent>(agent);
var agentVersion2 = agent.GetService<AgentVersion>()!;
var agentVersion2 = agent.GetService<ProjectsAgentVersion>()!;
Assert.NotNull(agentVersion);
if (agentVersion2.Definition is PromptAgentDefinition promptDef)
if (agentVersion2.Definition is DeclarativeAgentDefinition promptDef)
{
Assert.NotEmpty(promptDef.Tools);
Assert.Equal(10, promptDef.Tools.Count);
@@ -624,19 +624,19 @@ public sealed class AzureAIProjectChatClientExtensionsTests
functionDescription: "Gets the user's name, as used for friendly address."
);
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var definition = new DeclarativeAgentDefinition("test-model") { Instructions = "Test" };
definition.Tools.Add(functionTool);
// Generate response with the declarative function
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var definitionResponse = new DeclarativeAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(functionTool);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
var options = new ProjectsAgentVersionCreationOptions(definition);
// Act
var agentVersion = (await testClient.Client.Agents.CreateAgentVersionAsync("test-agent", options)).Value;
var agentVersion = (await testClient.Client.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", options)).Value;
var agent = testClient.Client.AsAIAgent(agentVersion);
// Assert
@@ -652,7 +652,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var definition = new DeclarativeAgentDefinition("test-model") { Instructions = "Test" };
// Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration
using var doc = JsonDocument.Parse("{}");
@@ -661,10 +661,10 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Add to definition
definition.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
var options = new AgentVersionCreationOptions(definition);
var options = new ProjectsAgentVersionCreationOptions(definition);
// Act
var agentVersion = (await testClient.Client.Agents.CreateAgentVersionAsync("test-agent", options)).Value;
var agentVersion = (await testClient.Client.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", options)).Value;
var agent = testClient.Client.AsAIAgent(agentVersion);
// Assert
@@ -679,7 +679,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public async Task AsAIAgent_WithDeclarativeFunctionInDefinition_AcceptsDeclarativeFunctionAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var definition = new DeclarativeAgentDefinition("test-model") { Instructions = "Test" };
// Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration
using var doc = JsonDocument.Parse("{}");
@@ -689,15 +689,15 @@ public sealed class AzureAIProjectChatClientExtensionsTests
definition.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
// Generate response with the declarative function
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var definitionResponse = new DeclarativeAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
var options = new ProjectsAgentVersionCreationOptions(definition);
// Act
var agentVersion = (await testClient.Client.Agents.CreateAgentVersionAsync("test-agent", options)).Value;
var agentVersion = (await testClient.Client.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", options)).Value;
var agent = testClient.Client.AsAIAgent(agentVersion);
// Assert
@@ -758,7 +758,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
int factoryCallCount = 0;
// Act
@@ -785,7 +785,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent1 = client.AsAIAgent(
@@ -904,7 +904,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Arrange
var aiProjectClient = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
var agentVersion = (await aiProjectClient.Agents.CreateAgentVersionAsync("test-agent", new AgentVersionCreationOptions(new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }))).Value;
var agentVersion = (await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", new ProjectsAgentVersionCreationOptions(new DeclarativeAgentDefinition("test-model") { Instructions = "Test instructions" }))).Value;
// Act
var agent = aiProjectClient.AsAIAgent(agentVersion);
@@ -1043,21 +1043,21 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#endregion
#region GetService<AgentRecord> Tests
#region GetService<ProjectsAgentRecord> Tests
/// <summary>
/// Verify that GetService returns AgentRecord for agents created from AgentRecord.
/// Verify that GetService returns ProjectsAgentRecord for agents created from ProjectsAgentRecord.
/// </summary>
[Fact]
public void GetService_WithAgentRecord_ReturnsAgentRecord()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
var retrievedRecord = agent.GetService<AgentRecord>();
var retrievedRecord = agent.GetService<ProjectsAgentRecord>();
// Assert
Assert.NotNull(retrievedRecord);
@@ -1065,7 +1065,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that GetService returns null for AgentRecord when agent is created from AgentReference.
/// Verify that GetService returns null for ProjectsAgentRecord when agent is created from AgentReference.
/// </summary>
[Fact]
public void GetService_WithAgentReference_ReturnsNullForAgentRecord()
@@ -1076,7 +1076,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Act
var agent = client.AsAIAgent(agentReference);
var retrievedRecord = agent.GetService<AgentRecord>();
var retrievedRecord = agent.GetService<ProjectsAgentRecord>();
// Assert
Assert.Null(retrievedRecord);
@@ -1084,21 +1084,21 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#endregion
#region GetService<AgentVersion> Tests
#region GetService<ProjectsAgentVersion> Tests
/// <summary>
/// Verify that GetService returns AgentVersion for agents created from AgentVersion.
/// Verify that GetService returns ProjectsAgentVersion for agents created from ProjectsAgentVersion.
/// </summary>
[Fact]
public void GetService_WithAgentVersion_ReturnsAgentVersion()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
var retrievedVersion = agent.GetService<AgentVersion>();
var retrievedVersion = agent.GetService<ProjectsAgentVersion>();
// Assert
Assert.NotNull(retrievedVersion);
@@ -1106,7 +1106,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that GetService returns null for AgentVersion when agent is created from AgentReference.
/// Verify that GetService returns null for ProjectsAgentVersion when agent is created from AgentReference.
/// </summary>
[Fact]
public void GetService_WithAgentReference_ReturnsNullForAgentVersion()
@@ -1117,7 +1117,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Act
var agent = client.AsAIAgent(agentReference);
var retrievedVersion = agent.GetService<AgentVersion>();
var retrievedVersion = agent.GetService<ProjectsAgentVersion>();
// Assert
Assert.Null(retrievedVersion);
@@ -1128,14 +1128,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#region ChatClientMetadata Tests
/// <summary>
/// Verify that ChatClientMetadata is properly populated for agents created from AgentRecord.
/// Verify that ChatClientMetadata is properly populated for agents created from ProjectsAgentRecord.
/// </summary>
[Fact]
public void ChatClientMetadata_WithAgentRecord_IsPopulatedCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
@@ -1147,18 +1147,18 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that ChatClientMetadata.DefaultModelId is set from PromptAgentDefinition model property.
/// Verify that ChatClientMetadata.DefaultModelId is set from DeclarativeAgentDefinition model property.
/// </summary>
[Fact]
public void ChatClientMetadata_WithPromptAgentDefinition_SetsDefaultModelIdFromModel()
public void ChatClientMetadata_WithDeclarativeAgentDefinition_SetsDefaultModelIdFromModel()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var definition = new PromptAgentDefinition("gpt-4-turbo")
var definition = new DeclarativeAgentDefinition("gpt-4-turbo")
{
Instructions = "Test instructions"
};
AgentRecord agentRecord = this.CreateTestAgentRecord(definition);
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord(definition);
// Act
var agent = client.AsAIAgent(agentRecord);
@@ -1172,14 +1172,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that ChatClientMetadata is properly populated for agents created from AgentVersion.
/// Verify that ChatClientMetadata is properly populated for agents created from ProjectsAgentVersion.
/// </summary>
[Fact]
public void ChatClientMetadata_WithAgentVersion_IsPopulatedCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
@@ -1188,7 +1188,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
// Assert
Assert.NotNull(metadata);
Assert.NotNull(metadata.DefaultModelId);
Assert.Equal((agentVersion.Definition as PromptAgentDefinition)!.Model, metadata.DefaultModelId);
Assert.Equal((agentVersion.Definition as DeclarativeAgentDefinition)!.Model, metadata.DefaultModelId);
}
#endregion
@@ -1216,14 +1216,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that GetService returns null for AgentReference when agent is created from AgentRecord.
/// Verify that GetService returns null for AgentReference when agent is created from ProjectsAgentRecord.
/// </summary>
[Fact]
public void GetService_WithAgentRecord_ReturnsAlsoAgentReference()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
@@ -1235,14 +1235,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that GetService returns null for AgentReference when agent is created from AgentVersion.
/// Verify that GetService returns null for AgentReference when agent is created from ProjectsAgentVersion.
/// </summary>
[Fact]
public void GetService_WithAgentVersion_ReturnsAlsoAgentReference()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
@@ -1278,14 +1278,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#region Empty Version and ID Handling Tests
/// <summary>
/// Verify that AsAIAgent with AgentRecord handles empty version by using "latest" as fallback.
/// Verify that AsAIAgent with ProjectsAgentRecord handles empty version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
AgentRecord agentRecord = this.CreateTestAgentRecordWithEmptyVersion();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecordWithEmptyVersion();
// Act
var agent = client.AsAIAgent(agentRecord);
@@ -1297,14 +1297,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion handles empty version by using "latest" as fallback.
/// Verify that AsAIAgent with ProjectsAgentVersion handles empty version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
AgentVersion agentVersion = this.CreateTestAgentVersionWithEmptyVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersionWithEmptyVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
@@ -1316,14 +1316,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord handles whitespace-only version by using "latest" as fallback.
/// Verify that AsAIAgent with ProjectsAgentRecord handles whitespace-only version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecordWhitespaceVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
AgentRecord agentRecord = this.CreateTestAgentRecordWithWhitespaceVersion();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecordWithWhitespaceVersion();
// Act
var agent = client.AsAIAgent(agentRecord);
@@ -1335,14 +1335,14 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion handles whitespace-only version by using "latest" as fallback.
/// Verify that AsAIAgent with ProjectsAgentVersion handles whitespace-only version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersionWhitespaceVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
AgentVersion agentVersion = this.CreateTestAgentVersionWithWhitespaceVersion();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersionWithWhitespaceVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
@@ -1364,11 +1364,11 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public void AsAIAgent_WithServerHostedTools_AddsToolsToAgentOptions()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
DeclarativeAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(new HostedWebSearchTool().GetService<ResponseTool>() ?? new HostedWebSearchTool().AsOpenAIResponseTool());
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson(agentDefinition: definition)))!;
ProjectsAgentVersion agentVersion = ModelReaderWriter.Read<ProjectsAgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson(agentDefinition: definition)))!;
// Act - no tools provided, but requireInvocableTools is false when no tools param is passed
FoundryAgent agent = client.AsAIAgent(agentVersion);
@@ -1385,7 +1385,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
/// <summary>
/// Creates a test AIProjectClient with fake behavior.
/// </summary>
private FakeAgentClient CreateTestAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
private FakeAgentClient CreateTestAgentClient(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse);
}
@@ -1395,7 +1395,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
/// Used for tests that exercise the protocol-method code path (CreateAgentVersion).
/// The returned client must be disposed to clean up the underlying HttpClient/handler.
/// </summary>
private static DisposableTestClient CreateTestAgentClientWithHandler(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
private static DisposableTestClient CreateTestAgentClientWithHandler(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null)
{
var responseJson = TestDataUtil.GetAgentVersionResponseJson(agentName, agentDefinitionResponse, instructions, description);
@@ -1439,59 +1439,59 @@ public sealed class AzureAIProjectChatClientExtensionsTests
}
/// <summary>
/// Creates a test AgentRecord for testing.
/// Creates a test ProjectsAgentRecord for testing.
/// </summary>
private AgentRecord CreateTestAgentRecord(AgentDefinition? agentDefinition = null)
private ProjectsAgentRecord CreateTestAgentRecord(ProjectsAgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!;
return ModelReaderWriter.Read<ProjectsAgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AIProjectClient with empty version fields for testing hosted MCP agents.
/// </summary>
private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true);
}
/// <summary>
/// Creates a test AgentRecord with empty version for testing hosted MCP agents.
/// Creates a test ProjectsAgentRecord with empty version for testing hosted MCP agents.
/// </summary>
private AgentRecord CreateTestAgentRecordWithEmptyVersion(AgentDefinition? agentDefinition = null)
private ProjectsAgentRecord CreateTestAgentRecordWithEmptyVersion(ProjectsAgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!;
return ModelReaderWriter.Read<ProjectsAgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AgentVersion with empty version for testing hosted MCP agents.
/// Creates a test ProjectsAgentVersion with empty version for testing hosted MCP agents.
/// </summary>
private AgentVersion CreateTestAgentVersionWithEmptyVersion()
private ProjectsAgentVersion CreateTestAgentVersionWithEmptyVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!;
return ModelReaderWriter.Read<ProjectsAgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!;
}
/// <summary>
/// Creates a test AIProjectClient with whitespace-only version fields for testing hosted MCP agents.
/// </summary>
private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace);
}
/// <summary>
/// Creates a test AgentRecord with whitespace-only version for testing hosted MCP agents.
/// Creates a test ProjectsAgentRecord with whitespace-only version for testing hosted MCP agents.
/// </summary>
private AgentRecord CreateTestAgentRecordWithWhitespaceVersion(AgentDefinition? agentDefinition = null)
private ProjectsAgentRecord CreateTestAgentRecordWithWhitespaceVersion(ProjectsAgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!;
return ModelReaderWriter.Read<ProjectsAgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AgentVersion with whitespace-only version for testing hosted MCP agents.
/// Creates a test ProjectsAgentVersion with whitespace-only version for testing hosted MCP agents.
/// </summary>
private AgentVersion CreateTestAgentVersionWithWhitespaceVersion()
private ProjectsAgentVersion CreateTestAgentVersionWithWhitespaceVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!;
return ModelReaderWriter.Read<ProjectsAgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!;
}
private const string OpenAPISpec = """
@@ -1525,11 +1525,11 @@ public sealed class AzureAIProjectChatClientExtensionsTests
""";
/// <summary>
/// Creates a test AgentVersion for testing.
/// Creates a test ProjectsAgentVersion for testing.
/// </summary>
private AgentVersion CreateTestAgentVersion()
private ProjectsAgentVersion CreateTestAgentVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!;
return ModelReaderWriter.Read<ProjectsAgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!;
}
/// <summary>
@@ -1547,11 +1547,11 @@ public sealed class AzureAIProjectChatClientExtensionsTests
/// </summary>
private sealed class FakeAgentClient : AIProjectClient
{
public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal)
public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal)
{
// Handle backward compatibility with bool parameter
var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode;
this.Agents = new FakeAgentsClient(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode);
this.AgentAdministrationClient = new FakeAgentsClient(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode);
}
public override ClientConnection GetConnection(string connectionId)
@@ -1559,17 +1559,17 @@ public sealed class AzureAIProjectChatClientExtensionsTests
return new ClientConnection("fake-connection-id", "http://localhost", ClientPipeline.Create(), CredentialKind.None);
}
public override AgentsClient Agents { get; }
public override AgentAdministrationClient AgentAdministrationClient { get; }
private sealed class FakeAgentsClient : AgentsClient
private sealed class FakeAgentsClient : AgentAdministrationClient
{
private readonly string? _agentName;
private readonly string? _instructions;
private readonly string? _description;
private readonly AgentDefinition? _agentDefinition;
private readonly ProjectsAgentDefinition? _agentDefinition;
private readonly VersionMode _versionMode;
public FakeAgentsClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal)
public FakeAgentsClient(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal)
{
this._agentName = agentName;
this._instructions = instructions;
@@ -1601,44 +1601,44 @@ public sealed class AzureAIProjectChatClientExtensionsTests
public override ClientResult GetAgent(string agentName, RequestOptions options)
{
var responseJson = this.GetAgentResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
return ClientResult.FromValue(ModelReaderWriter.Read<ProjectsAgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult<AgentRecord> GetAgent(string agentName, CancellationToken cancellationToken = default)
public override ClientResult<ProjectsAgentRecord> GetAgent(string agentName, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
return ClientResult.FromValue(ModelReaderWriter.Read<ProjectsAgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult> GetAgentAsync(string agentName, RequestOptions options)
{
var responseJson = this.GetAgentResponseJson();
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<ProjectsAgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task<ClientResult<AgentRecord>> GetAgentAsync(string agentName, CancellationToken cancellationToken = default)
public override Task<ClientResult<ProjectsAgentRecord>> GetAgentAsync(string agentName, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<ProjectsAgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
public override ClientResult<ProjectsAgentVersion> CreateAgentVersion(string agentName, ProjectsAgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
return ClientResult.FromValue(ModelReaderWriter.Read<ProjectsAgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
public override Task<ClientResult<ProjectsAgentVersion>> CreateAgentVersionAsync(string agentName, ProjectsAgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<ProjectsAgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
}
}
private static PromptAgentDefinition GeneratePromptDefinitionResponse(PromptAgentDefinition inputDefinition, List<AITool>? tools)
private static DeclarativeAgentDefinition GeneratePromptDefinitionResponse(DeclarativeAgentDefinition inputDefinition, List<AITool>? tools)
{
var definitionResponse = new PromptAgentDefinition(inputDefinition.Model) { Instructions = inputDefinition.Instructions };
var definitionResponse = new DeclarativeAgentDefinition(inputDefinition.Model) { Instructions = inputDefinition.Instructions };
if (tools is not null)
{
foreach (var tool in tools)
@@ -29,7 +29,7 @@ internal static class TestDataUtil
/// <summary>
/// Gets the agent response JSON with optional placeholder replacements applied.
/// </summary>
public static string GetAgentResponseJson(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
public static string GetAgentResponseJson(string? agentName = null, ProjectsAgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentResponseJson;
json = ApplyAgentName(json, agentName);
@@ -42,7 +42,7 @@ internal static class TestDataUtil
/// <summary>
/// Gets the agent version response JSON with optional placeholder replacements applied.
/// </summary>
public static string GetAgentVersionResponseJson(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
public static string GetAgentVersionResponseJson(string? agentName = null, ProjectsAgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentVersionResponseJson;
json = ApplyAgentName(json, agentName);
@@ -55,7 +55,7 @@ internal static class TestDataUtil
/// <summary>
/// Gets the agent version response JSON with empty version and ID fields for testing hosted agents like MCP agents.
/// </summary>
public static string GetAgentVersionResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
public static string GetAgentVersionResponseJsonWithEmptyVersion(string? agentName = null, ProjectsAgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentVersionResponseJson;
json = ApplyAgentName(json, agentName);
@@ -71,7 +71,7 @@ internal static class TestDataUtil
/// <summary>
/// Gets the agent response JSON with empty version and ID fields in the latest version for testing hosted agents like MCP agents.
/// </summary>
public static string GetAgentResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
public static string GetAgentResponseJsonWithEmptyVersion(string? agentName = null, ProjectsAgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentResponseJson;
json = ApplyAgentName(json, agentName);
@@ -87,7 +87,7 @@ internal static class TestDataUtil
/// <summary>
/// Gets the agent version response JSON with whitespace-only version and ID fields for testing hosted agents like MCP agents.
/// </summary>
public static string GetAgentVersionResponseJsonWithWhitespaceVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
public static string GetAgentVersionResponseJsonWithWhitespaceVersion(string? agentName = null, ProjectsAgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentVersionResponseJson;
json = ApplyAgentName(json, agentName);
@@ -103,7 +103,7 @@ internal static class TestDataUtil
/// <summary>
/// Gets the agent response JSON with whitespace-only version and ID fields in the latest version for testing hosted agents like MCP agents.
/// </summary>
public static string GetAgentResponseJsonWithWhitespaceVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
public static string GetAgentResponseJsonWithWhitespaceVersion(string? agentName = null, ProjectsAgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentResponseJson;
json = ApplyAgentName(json, agentName);
@@ -119,7 +119,7 @@ internal static class TestDataUtil
/// <summary>
/// Gets the OpenAI default response JSON with optional placeholder replacements applied.
/// </summary>
public static string GetOpenAIDefaultResponseJson(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
public static string GetOpenAIDefaultResponseJson(string? agentName = null, ProjectsAgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_openAIDefaultResponseJson;
json = ApplyAgentName(json, agentName);
@@ -138,7 +138,7 @@ internal static class TestDataUtil
return json;
}
private static string ApplyAgentDefinition(string json, AgentDefinition? definition)
private static string ApplyAgentDefinition(string json, ProjectsAgentDefinition? definition)
{
return (definition is not null)
? json.Replace(AgentDefinitionPlaceholder, ModelReaderWriter.Write(definition).ToString())
@@ -0,0 +1,382 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
/// <summary>
/// Unit tests for <see cref="AgentClassSkill"/> and <see cref="AgentInMemorySkillsSource"/>.
/// </summary>
public sealed class AgentClassSkillTests
{
[Fact]
public void Resources_DefaultsToNull_WhenNotOverridden()
{
// Arrange
var skill = new MinimalClassSkill();
// Act & Assert
Assert.Null(skill.Resources);
}
[Fact]
public void Scripts_DefaultsToNull_WhenNotOverridden()
{
// Arrange
var skill = new MinimalClassSkill();
// Act & Assert
Assert.Null(skill.Scripts);
}
[Fact]
public void Resources_ReturnsOverriddenList_WhenOverridden()
{
// Arrange
var skill = new FullClassSkill();
// Act
var resources = skill.Resources;
// Assert
Assert.Single(resources!);
Assert.Equal("test-resource", resources![0].Name);
}
[Fact]
public void Scripts_ReturnsOverriddenList_WhenOverridden()
{
// Arrange
var skill = new FullClassSkill();
// Act
var scripts = skill.Scripts;
// Assert
Assert.Single(scripts!);
Assert.Equal("TestScript", scripts![0].Name);
}
[Fact]
public void ResourcesAndScripts_CanBeLazyLoaded_AndCached()
{
// Arrange
var skill = new LazyLoadedSkill();
// Act & Assert
Assert.Equal(0, skill.ResourceCreationCount);
Assert.Equal(0, skill.ScriptCreationCount);
var firstResources = skill.Resources;
var firstScripts = skill.Scripts;
var secondResources = skill.Resources;
var secondScripts = skill.Scripts;
Assert.Single(firstResources!);
Assert.Single(firstScripts!);
Assert.Same(firstResources, secondResources);
Assert.Same(firstScripts, secondScripts);
Assert.Equal(1, skill.ResourceCreationCount);
Assert.Equal(1, skill.ScriptCreationCount);
}
[Fact]
public void Name_Content_ReturnClassDefinedValues()
{
// Arrange
var skill = new MinimalClassSkill();
// Act & Assert
Assert.Equal("minimal", skill.Frontmatter.Name);
Assert.Contains("<instructions>", skill.Content);
Assert.Contains("Minimal skill body.", skill.Content);
Assert.Contains("</instructions>", skill.Content);
}
[Fact]
public void Content_ReturnsSynthesizedXmlDocument()
{
// Arrange
var skill = new MinimalClassSkill();
// Act & Assert
Assert.Contains("<name>minimal</name>", skill.Content);
Assert.Contains("<description>A minimal skill.</description>", skill.Content);
Assert.Contains("<instructions>", skill.Content);
Assert.Contains("Minimal skill body.", skill.Content);
}
[Fact]
public async Task AgentInMemorySkillsSource_ReturnsAllSkillsAsync()
{
// Arrange
var skills = new AgentClassSkill[] { new MinimalClassSkill(), new FullClassSkill() };
var source = new AgentInMemorySkillsSource(skills);
// Act
var result = await source.GetSkillsAsync(CancellationToken.None);
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("minimal", result[0].Frontmatter.Name);
Assert.Equal("full", result[1].Frontmatter.Name);
}
[Fact]
public void AgentClassSkill_InvalidFrontmatter_ThrowsArgumentException()
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new AgentSkillFrontmatter("INVALID-NAME", "An invalid skill."));
}
[Fact]
public void SkillWithOnlyResources_HasNullScripts()
{
// Arrange
var skill = new ResourceOnlySkill();
// Act & Assert
Assert.Single(skill.Resources!);
Assert.Null(skill.Scripts);
}
[Fact]
public void SkillWithOnlyScripts_HasNullResources()
{
// Arrange
var skill = new ScriptOnlySkill();
// Act & Assert
Assert.Null(skill.Resources);
Assert.Single(skill.Scripts!);
}
[Fact]
public void Content_ReturnsCachedInstance_OnRepeatedAccess()
{
// Arrange
var skill = new FullClassSkill();
// Act
var first = skill.Content;
var second = skill.Content;
// Assert
Assert.Same(first, second);
}
[Fact]
public void Content_IncludesParametersSchema_WhenScriptsHaveParameters()
{
// Arrange
var skill = new FullClassSkill();
// Act
var content = skill.Content;
// Assert — scripts with typed parameters should have their schema included
Assert.Contains("parameters_schema", content);
Assert.Contains("value", content);
}
[Fact]
public void Content_IncludesDerivedResources_WhenResourcesUseBaseTypeOverrides()
{
// Arrange
var skill = new DerivedResourceSkill();
// Act
var content = skill.Content;
// Assert
Assert.Contains("<resources>", content);
Assert.Contains("custom-resource", content);
Assert.Contains("Custom resource description.", content);
}
[Fact]
public void Content_IncludesDerivedScripts_WhenScriptsUseBaseTypeOverrides()
{
// Arrange
var skill = new DerivedScriptSkill();
// Act
var content = skill.Content;
// Assert
Assert.Contains("<scripts>", content);
Assert.Contains("custom-script", content);
Assert.Contains("Custom script description.", content);
}
[Fact]
public void Content_OmitsParametersSchema_WhenDerivedScriptDoesNotProvideOne()
{
// Arrange
var skill = new DerivedScriptSkill();
// Act
var content = skill.Content;
// Assert
Assert.DoesNotContain("parameters_schema", content);
}
#region Test skill classes
private sealed class MinimalClassSkill : AgentClassSkill
{
public override AgentSkillFrontmatter Frontmatter { get; } = new("minimal", "A minimal skill.");
protected override string Instructions => "Minimal skill body.";
public override IReadOnlyList<AgentSkillResource>? Resources => null;
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
}
private sealed class FullClassSkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillResource>? _resources;
private IReadOnlyList<AgentSkillScript>? _scripts;
public override AgentSkillFrontmatter Frontmatter { get; } = new("full", "A full skill with resources and scripts.");
protected override string Instructions => "Full skill body.";
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
[
CreateResource("test-resource", "resource content"),
];
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
[
CreateScript("TestScript", TestScript),
];
private static string TestScript(double value) =>
JsonSerializer.Serialize(new { result = value * 2 });
}
private sealed class ResourceOnlySkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillResource>? _resources;
public override AgentSkillFrontmatter Frontmatter { get; } = new("resource-only", "Skill with resources only.");
protected override string Instructions => "Body.";
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
[
CreateResource("data", "some data"),
];
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
}
private sealed class ScriptOnlySkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillScript>? _scripts;
public override AgentSkillFrontmatter Frontmatter { get; } = new("script-only", "Skill with scripts only.");
protected override string Instructions => "Body.";
public override IReadOnlyList<AgentSkillResource>? Resources => null;
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
[
CreateScript("ToUpper", (string input) => input.ToUpperInvariant()),
];
}
private sealed class DerivedResourceSkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillResource>? _resources;
public override AgentSkillFrontmatter Frontmatter { get; } = new("derived-resource", "Skill with a derived resource type.");
protected override string Instructions => "Body.";
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??=
[
new CustomResource("custom-resource", "Custom resource description."),
];
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
}
private sealed class DerivedScriptSkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillScript>? _scripts;
public override AgentSkillFrontmatter Frontmatter { get; } = new("derived-script", "Skill with a derived script type.");
protected override string Instructions => "Body.";
public override IReadOnlyList<AgentSkillResource>? Resources => null;
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??=
[
new CustomScript("custom-script", "Custom script description."),
];
}
private sealed class LazyLoadedSkill : AgentClassSkill
{
private IReadOnlyList<AgentSkillResource>? _resources;
private IReadOnlyList<AgentSkillScript>? _scripts;
public override AgentSkillFrontmatter Frontmatter { get; } = new("lazy-loaded", "Skill with lazily created resources and scripts.");
protected override string Instructions => "Body.";
public int ResourceCreationCount { get; private set; }
public int ScriptCreationCount { get; private set; }
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources ??= this.CreateResources();
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts ??= this.CreateScripts();
private IReadOnlyList<AgentSkillResource> CreateResources()
{
this.ResourceCreationCount++;
return [CreateResource("lazy-resource", "resource content")];
}
private IReadOnlyList<AgentSkillScript> CreateScripts()
{
this.ScriptCreationCount++;
return [CreateScript("LazyScript", () => "done")];
}
}
private sealed class CustomResource : AgentSkillResource
{
public CustomResource(string name, string? description = null)
: base(name, description)
{
}
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
=> Task.FromResult<object?>("resource-value");
}
private sealed class CustomScript : AgentSkillScript
{
public CustomScript(string name, string? description = null)
: base(name, description)
{
}
public override Task<object?> RunAsync(AgentSkill skill, Extensions.AI.AIFunctionArguments arguments, CancellationToken cancellationToken = default)
=> Task.FromResult<object?>("script-result");
}
#endregion
}
@@ -851,6 +851,61 @@ public sealed class AgentSkillsProviderTests : IDisposable
Assert.Contains("First instructions.", content!.ToString()!);
}
[Fact]
public async Task Constructor_ClassSkillsParams_ProvidesSkillsAsync()
{
// Arrange
var skill = new TestClassSkill("class-a", "Class A", "Class instructions.");
var provider = new AgentSkillsProvider(skill);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert
Assert.NotNull(result.Instructions);
Assert.Contains("class-a", result.Instructions);
}
[Fact]
public async Task Constructor_ClassSkillsEnumerable_ProvidesSkillsAsync()
{
// Arrange
var skills = new List<AgentClassSkill>
{
new TestClassSkill("enum-class-a", "Class A", "Instructions A."),
new TestClassSkill("enum-class-b", "Class B", "Instructions B."),
};
var provider = new AgentSkillsProvider(skills);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert
Assert.NotNull(result.Instructions);
Assert.Contains("enum-class-a", result.Instructions);
Assert.Contains("enum-class-b", result.Instructions);
}
[Fact]
public async Task Constructor_ClassSkills_DeduplicatesAsync()
{
// Arrange — two class skills with the same name
var skill1 = new TestClassSkill("dup-class", "First", "First instructions.");
var skill2 = new TestClassSkill("dup-class", "Second", "Second instructions.");
var provider = new AgentSkillsProvider(skill1, skill2);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
var loadSkillTool = result.Tools!.First(t => t.Name == "load_skill") as AIFunction;
var content = await loadSkillTool!.InvokeAsync(new AIFunctionArguments(new Dictionary<string, object?> { ["skillName"] = "dup-class" }));
// Assert — only first occurrence survives
Assert.Contains("First instructions.", content!.ToString()!);
}
/// <summary>
/// A test skill source that counts how many times <see cref="GetSkillsAsync"/> is called.
/// </summary>
@@ -872,4 +927,23 @@ public sealed class AgentSkillsProviderTests : IDisposable
return Task.FromResult(this._skills);
}
}
private sealed class TestClassSkill : AgentClassSkill
{
private readonly string _instructions;
public TestClassSkill(string name, string description, string instructions)
{
this.Frontmatter = new AgentSkillFrontmatter(name, description);
this._instructions = instructions;
}
public override AgentSkillFrontmatter Frontmatter { get; }
protected override string Instructions => this._instructions;
public override IReadOnlyList<AgentSkillResource>? Resources => null;
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
}
}
@@ -35,13 +35,13 @@ internal abstract class AgentProvider(IConfiguration configuration)
{
Uri foundryEndpoint = new(this.GetSetting(TestSettings.AzureAIProjectEndpoint));
await foreach (AgentVersion agent in this.CreateAgentsAsync(foundryEndpoint))
await foreach (ProjectsAgentVersion agent in this.CreateAgentsAsync(foundryEndpoint))
{
Console.WriteLine($"Created agent: {agent.Name}:{agent.Version}");
}
}
protected abstract IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint);
protected abstract IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint);
protected string GetSetting(string settingName) =>
configuration[settingName] ??
@@ -14,7 +14,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class FunctionToolAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
MenuPlugin menuPlugin = new();
AIFunction[] functions =
@@ -33,9 +33,9 @@ internal sealed class FunctionToolAgentProvider(IConfiguration configuration) :
agentDescription: "Provides information about the restaurant menu");
}
private PromptAgentDefinition DefineMenuAgent(AIFunction[] functions)
private DeclarativeAgentDefinition DefineMenuAgent(AIFunction[] functions)
{
PromptAgentDefinition agentDefinition =
DeclarativeAgentDefinition agentDefinition =
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
@@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class MarketingAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
@@ -35,7 +35,7 @@ internal sealed class MarketingAgentProvider(IConfiguration configuration) : Age
agentDescription: "Editor agent for Marketing workflow");
}
private PromptAgentDefinition DefineAnalystAgent() =>
private DeclarativeAgentDefinition DefineAnalystAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
@@ -47,13 +47,13 @@ internal sealed class MarketingAgentProvider(IConfiguration configuration) : Age
""",
Tools =
{
//AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
//ProjectsAgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
// new BingGroundingSearchToolParameters(
// [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))]))
}
};
private PromptAgentDefinition DefineWriterAgent() =>
private DeclarativeAgentDefinition DefineWriterAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
@@ -64,7 +64,7 @@ internal sealed class MarketingAgentProvider(IConfiguration configuration) : Age
"""
};
private PromptAgentDefinition DefineEditorAgent() =>
private DeclarativeAgentDefinition DefineEditorAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
@@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class MathChatAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
@@ -29,7 +29,7 @@ internal sealed class MathChatAgentProvider(IConfiguration configuration) : Agen
agentDescription: "Teacher agent for MathChat workflow");
}
private PromptAgentDefinition DefineStudentAgent() =>
private DeclarativeAgentDefinition DefineStudentAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
@@ -41,7 +41,7 @@ internal sealed class MathChatAgentProvider(IConfiguration configuration) : Agen
"""
};
private PromptAgentDefinition DefineTeacherAgent() =>
private DeclarativeAgentDefinition DefineTeacherAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
@@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class PoemAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
@@ -23,7 +23,7 @@ internal sealed class PoemAgentProvider(IConfiguration configuration) : AgentPro
agentDescription: "Authors original poems");
}
private PromptAgentDefinition DefinePoemAgent() =>
private DeclarativeAgentDefinition DefinePoemAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
@@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class TestAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
@@ -23,6 +23,6 @@ internal sealed class TestAgentProvider(IConfiguration configuration) : AgentPro
agentDescription: "Basic agent");
}
private PromptAgentDefinition DefineMenuAgent() =>
private DeclarativeAgentDefinition DefineMenuAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName));
}
@@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class VisionAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
@@ -23,7 +23,7 @@ internal sealed class VisionAgentProvider(IConfiguration configuration) : AgentP
agentDescription: "Use computer vision to describe an image or document.");
}
private PromptAgentDefinition DefineVisionAgent() =>
private DeclarativeAgentDefinition DefineVisionAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using FluentAssertions;
namespace Microsoft.Agents.AI.Workflows.UnitTests;
public class AggregatingExecutorTests
{
[Fact]
public async Task AggregatingExecutor_HandleAsync_AggregatesIncrementallyAsync()
{
AggregatingExecutor<string, string> executor = new("sum", (aggregate, input) =>
aggregate == null ? input : $"{aggregate}+{input}");
TestWorkflowContext context = new(executor.Id);
string? result1 = await executor.HandleAsync("a", context, default);
string? result2 = await executor.HandleAsync("b", context, default);
string? result3 = await executor.HandleAsync("c", context, default);
result1.Should().Be("a");
result2.Should().Be("a+b");
result3.Should().Be("a+b+c");
}
[Fact]
public async Task AggregatingExecutor_HandleAsync_FirstCallReceivesNullAggregateAsync()
{
string? receivedAggregate = "sentinel";
AggregatingExecutor<string, string> executor = new("first-call", (aggregate, input) =>
{
receivedAggregate = aggregate;
return input;
});
TestWorkflowContext context = new(executor.Id);
await executor.HandleAsync("hello", context, default);
receivedAggregate.Should().BeNull("the first invocation should receive a null aggregate for reference types");
}
[Fact]
public async Task AggregatingExecutor_HandleAsync_AggregatorReturningNullClearsStateAsync()
{
AggregatingExecutor<string, string> executor = new("nullable", (aggregate, input) =>
input == "clear" ? null : (aggregate ?? "") + input);
TestWorkflowContext context = new(executor.Id);
string? result1 = await executor.HandleAsync("a", context, default);
result1.Should().Be("a");
string? result2 = await executor.HandleAsync("clear", context, default);
result2.Should().BeNull("the aggregator returned null to clear the state");
// After clearing, the next call should receive null aggregate again
string? result3 = await executor.HandleAsync("b", context, default);
result3.Should().Be("b", "the aggregate should restart from null after being cleared");
}
[Fact]
public async Task AggregatingExecutor_HandleAsync_PersistsStateBetweenCallsAsync()
{
AggregatingExecutor<string, string> executor = new("counter", (aggregate, _) =>
aggregate == null ? "1" : $"{int.Parse(aggregate) + 1}");
TestWorkflowContext context = new(executor.Id);
for (int i = 1; i <= 5; i++)
{
string? result = await executor.HandleAsync("tick", context, default);
result.Should().Be($"{i}", "the aggregate should increment with each call");
}
}
}
@@ -132,6 +132,53 @@ public class InProcessExecutionTests
"both versions should produce the same number of agent events");
}
/// <summary>
/// This test checks that the logic around waiting for input and halting appropriately works right when the
/// workflow runs to halting before the EventStream is watched by the user.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWaitToTakeStreamAsync()
{
// Arrange: Create a simple agent that responds to messages
var agent = new SimpleTestAgent("test-agent");
var workflow = AgentWorkflowBuilder.BuildSequential(agent);
var inputMessage = new ChatMessage(ChatRole.User, "Hello");
// Act: Execute using streaming version with TurnToken
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new List<ChatMessage> { inputMessage });
// Send TurnToken to actually trigger execution (this is the key step)
bool messageSent = await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
messageSent.Should().BeTrue("TurnToken should be accepted");
while (await run.GetStatusAsync() != RunStatus.Idle)
{
await Task.Delay(200);
}
// Collect events
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert: The workflow should have executed and produced events
RunStatus status = await run.GetStatusAsync();
status.Should().Be(RunStatus.Idle, "workflow should complete execution");
events.Should().NotBeEmpty("workflow should produce events during execution");
// Check that we have agent execution events
var agentEvents = events.OfType<AgentResponseUpdateEvent>().ToList();
agentEvents.Should().NotBeEmpty("agent should have executed and produced update events");
// Check that we have output events
var outputEvents = events.OfType<WorkflowOutputEvent>().ToList();
outputEvents.Should().NotBeEmpty("workflow should produce output events");
}
/// <summary>
/// Simple test agent that echoes back the input message.
/// </summary>
@@ -0,0 +1,232 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Agents.AI.Workflows.InProc;
namespace Microsoft.Agents.AI.Workflows.UnitTests;
public class InProcessExecutorEventsTests
{
[SendsMessage(typeof(string[]))]
private sealed class EventTrackingExecutor(bool forwardMessages, string id) : Executor<IEnumerable<string>>(id)
{
public List<IEnumerable<string>> ReceivedMessages { get; } = [];
private int _checkpointingCalls;
public int CheckpointingCalls => this._checkpointingCalls;
private int _checkpointRestoredCalls;
public int CheckpointRestoredCalls => this._checkpointRestoredCalls;
private int _deliveryStartingCalls;
public int DeliveryStartingCalls => this._deliveryStartingCalls;
private int _deliveryFinishedAsyncCalls;
public int DeliveryFinishedCalls => this._deliveryFinishedAsyncCalls;
protected internal override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref this._checkpointingCalls);
return base.OnCheckpointingAsync(context, cancellationToken);
}
protected internal override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref this._checkpointRestoredCalls);
return base.OnCheckpointRestoredAsync(context, cancellationToken);
}
protected internal override ValueTask OnMessageDeliveryStartingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref this._deliveryStartingCalls);
return base.OnMessageDeliveryStartingAsync(context, cancellationToken);
}
protected internal override ValueTask OnMessageDeliveryFinishedAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref this._deliveryFinishedAsyncCalls);
return base.OnMessageDeliveryFinishedAsync(context, cancellationToken);
}
public override async ValueTask HandleAsync(IEnumerable<string> message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this.ReceivedMessages.Add(message);
if (forwardMessages)
{
foreach (string packedMessage in message)
{
await context.SendMessageAsync(new[] { packedMessage }, cancellationToken);
}
}
}
}
private sealed class TestFixture
{
public EventTrackingExecutor StartingExecutor { get; } = new(true, nameof(StartingExecutor));
public EventTrackingExecutor ReceivesMessage { get; } = new(false, nameof(ReceivesMessage));
public EventTrackingExecutor UninvokedExecutor { get; } = new(false, nameof(UninvokedExecutor));
public Workflow Workflow { get; }
public TestFixture()
{
this.Workflow = new WorkflowBuilder(this.StartingExecutor)
.AddEdge(this.StartingExecutor, this.ReceivesMessage)
// The uninvoked executor remains uninvoked because ReceivesMessage does not forward its incoming message
.AddEdge(this.ReceivesMessage, this.UninvokedExecutor)
.Build();
}
public const int StepsPerInputBatch = 2;
}
[Theory]
[InlineData(1, ExecutionEnvironment.InProcess_Lockstep)]
[InlineData(1, ExecutionEnvironment.InProcess_OffThread)]
internal async Task Test_InProcessExecution_InvokesDeliveryEventsOnceAsync(int messageCount, ExecutionEnvironment environment)
{
// Arrange
TestFixture fixture = new();
InProcessExecutionEnvironment executionEnvironment = environment.ToWorkflowExecutionEnvironment();
// Act
IEnumerable<string> batch = Enumerable.Range(1, messageCount).Select(i => $"Message_{i}");
await using StreamingRun streamingRun = await executionEnvironment.OpenStreamingAsync(fixture.Workflow);
await streamingRun.TrySendMessageAsync(batch);
await streamingRun.RunToCompletionAsync(ThrowOnError);
// Assert
fixture.StartingExecutor.DeliveryStartingCalls.Should().Be(1);
fixture.StartingExecutor.DeliveryFinishedCalls.Should().Be(1);
fixture.ReceivesMessage.DeliveryStartingCalls.Should().Be(1);
fixture.ReceivesMessage.DeliveryFinishedCalls.Should().Be(1);
fixture.UninvokedExecutor.DeliveryStartingCalls.Should().Be(0);
fixture.UninvokedExecutor.DeliveryFinishedCalls.Should().Be(0);
ExternalResponse? ThrowOnError(WorkflowEvent workflowEvent)
{
switch (workflowEvent)
{
case WorkflowErrorEvent workflowError:
Assert.Fail(workflowError.Exception?.ToString() ?? "Unknown error occurred while executing workflow.");
break;
case ExecutorFailedEvent executorFailed:
Assert.Fail(executorFailed.Data != null
? $"Executor {executorFailed.ExecutorId} failed with exception: {executorFailed.Data}"
: $"Executor {executorFailed.ExecutorId} failed with unknown error");
break;
}
return null;
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Test_InProcessExecution_InvokesCheckpointingEventIFFCheckpointingEnabledAsync(bool useCheckpointing)
{
// Arrange
TestFixture fixture = new();
InProcessExecutionEnvironment executionEnvironment = InProcessExecution.Default;
if (useCheckpointing)
{
executionEnvironment = executionEnvironment.WithCheckpointing(CheckpointManager.CreateInMemory());
}
// Act
string sessionId = Guid.NewGuid().ToString();
await using Run run = await executionEnvironment.RunAsync<string[]>(fixture.Workflow, ["Message"], sessionId);
// Assert
run.OutgoingEvents.OfType<WorkflowErrorEvent>().Should().BeEmpty();
run.OutgoingEvents.OfType<ExecutorFailedEvent>().Should().BeEmpty();
const int ExpectedSteps = TestFixture.StepsPerInputBatch;
run.OutgoingEvents.OfType<SuperStepCompletedEvent>().Should().HaveCount(ExpectedSteps);
int expectedCheckpoints = useCheckpointing ? ExpectedSteps : 0;
run.Checkpoints.Should().HaveCount(expectedCheckpoints);
fixture.StartingExecutor.CheckpointingCalls.Should().Be(expectedCheckpoints);
fixture.StartingExecutor.CheckpointRestoredCalls.Should().Be(0);
fixture.ReceivesMessage.CheckpointingCalls.Should().Be(expectedCheckpoints);
fixture.ReceivesMessage.CheckpointRestoredCalls.Should().Be(0);
fixture.UninvokedExecutor.CheckpointingCalls.Should().Be(0); // Uninvoked executors don't get "instantiated" in the workflow context
fixture.UninvokedExecutor.CheckpointRestoredCalls.Should().Be(0);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
//[InlineData(false, true)] - impossible to restore checkpoint with checkpointing disabled, will throw
public async Task Test_InProcessExecution_InvokesRestoredEventIFFRestoringCheckpointAsync(bool restoreCheckpoint)
{
// Arrange
TestFixture runFixture = new();
InProcessExecutionEnvironment executionEnvironment = InProcessExecution.Default.WithCheckpointing(CheckpointManager.CreateInMemory());
// Act
string sessionId = Guid.NewGuid().ToString();
Run run = await executionEnvironment.RunAsync<string[]>(runFixture.Workflow, ["Message"], sessionId);
// Assert
run.OutgoingEvents.OfType<WorkflowErrorEvent>().Should().BeEmpty();
run.OutgoingEvents.OfType<ExecutorFailedEvent>().Should().BeEmpty();
TestFixture validateFixture = runFixture;
// Act 2
int expectedCheckpoints = TestFixture.StepsPerInputBatch;
if (restoreCheckpoint)
{
expectedCheckpoints--; // We are restoring from the first one, so skip one
validateFixture = new();
run.Checkpoints.Should().HaveCount(TestFixture.StepsPerInputBatch);
CheckpointInfo firstCheckpoint = run.Checkpoints[0];
await run.DisposeAsync();
run = await executionEnvironment.ResumeAsync(validateFixture.Workflow, firstCheckpoint);
}
// Assert 2
if (restoreCheckpoint)
{
// Make sure the second run did not have failures
run.OutgoingEvents.OfType<WorkflowErrorEvent>().Should().BeEmpty();
run.OutgoingEvents.OfType<ExecutorFailedEvent>().Should().BeEmpty();
}
int expectedRestoreCalls = restoreCheckpoint ? 1 : 0;
validateFixture.StartingExecutor.CheckpointingCalls.Should().Be(expectedCheckpoints);
validateFixture.StartingExecutor.CheckpointRestoredCalls.Should().Be(expectedRestoreCalls);
validateFixture.ReceivesMessage.CheckpointingCalls.Should().Be(expectedCheckpoints);
validateFixture.ReceivesMessage.CheckpointRestoredCalls.Should().Be(expectedRestoreCalls);
validateFixture.UninvokedExecutor.CheckpointingCalls.Should().Be(0); // Uninvoked executors don't get "instantiated" in the workflow context
validateFixture.UninvokedExecutor.CheckpointRestoredCalls.Should().Be(0);
// Cleanup
await run.DisposeAsync();
}
}
@@ -0,0 +1,145 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Agents.AI.Workflows.Execution;
namespace Microsoft.Agents.AI.Workflows.UnitTests;
public sealed class InputWaiterTests : IDisposable
{
private readonly InputWaiter _waiter = new();
public void Dispose()
{
this._waiter.Dispose();
GC.SuppressFinalize(this);
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_CompletesAfterSignalAsync()
{
this._waiter.SignalInput();
// WaitForInputAsync should complete immediately since input was already signaled
Task waitTask = this._waiter.WaitForInputAsync(CancellationToken.None);
Task completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(1)));
completed.Should().BeSameAs(waitTask, "the wait task should complete before the timeout");
await waitTask;
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_BlocksUntilSignaledAsync()
{
Task waitTask = this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(5));
await Task.Delay(50);
waitTask.IsCompleted.Should().BeFalse("the waiter should block until input is signaled");
this._waiter.SignalInput();
Task completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(1)));
completed.Should().BeSameAs(waitTask, "the wait task should complete after being signaled");
await waitTask;
}
[Fact]
public void InputWaiter_SignalInput_DoubleSignalDoesNotThrow()
{
// Binary semaphore behavior: double signal should be idempotent
FluentActions.Invoking(() =>
{
this._waiter.SignalInput();
this._waiter.SignalInput();
}).Should().NotThrow("double signaling should be handled gracefully");
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_RespectsCancellationAsync()
{
using CancellationTokenSource cts = new();
Task waitTask = this._waiter.WaitForInputAsync(cts.Token);
cts.Cancel();
Func<Task> act = () => waitTask;
await act.Should().ThrowAsync<OperationCanceledException>();
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_DoesNotCompleteWhenNotSignaledAsync()
{
using CancellationTokenSource cts = new();
Task waitTask = this._waiter.WaitForInputAsync(cts.Token);
Task completed = await Task.WhenAny(waitTask, Task.Delay(100));
completed.Should().NotBeSameAs(waitTask, "the wait task should not complete when input is not signaled");
// Cancel and observe the pending task to avoid an unobserved exception on Dispose
cts.Cancel();
try { await waitTask; }
catch (OperationCanceledException) { }
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_CanBeSignaledMultipleTimesSequentiallyAsync()
{
// First signal/wait cycle
this._waiter.SignalInput();
await this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(1));
// Second signal/wait cycle
this._waiter.SignalInput();
await this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(1));
}
}
public class OutputFilterTests
{
private static OutputFilter CreateFilterWithOutputFrom(string outputExecutorId)
{
NoOpExecutor start = new("start");
NoOpExecutor end = new("end");
Workflow workflow = new WorkflowBuilder("start")
.AddEdge(start, end)
.WithOutputFrom(outputExecutorId == "end" ? end : start)
.Build();
return new OutputFilter(workflow);
}
[Fact]
public void OutputFilter_CanOutput_ReturnsTrueForRegisteredExecutor()
{
OutputFilter filter = CreateFilterWithOutputFrom("end");
filter.CanOutput("end", "some output").Should().BeTrue("the executor was registered via WithOutputFrom");
}
[Fact]
public void OutputFilter_CanOutput_ReturnsFalseForUnregisteredExecutor()
{
OutputFilter filter = CreateFilterWithOutputFrom("end");
filter.CanOutput("start", "some output").Should().BeFalse("start was not registered as an output executor");
}
[Fact]
public void OutputFilter_CanOutput_ReturnsFalseForNonExistentExecutor()
{
OutputFilter filter = CreateFilterWithOutputFrom("end");
filter.CanOutput("nonexistent", "some output").Should().BeFalse("an executor not in the workflow should not be an output executor");
}
private sealed class NoOpExecutor(string id) : Executor(id)
{
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
=> protocolBuilder.ConfigureRoutes(routeBuilder =>
routeBuilder.AddHandler<object>((msg, ctx) => ctx.SendMessageAsync(msg)));
}
}

Some files were not shown because too many files have changed in this diff Show More