Merge branch 'main' into features/3768-devui-aspire-integration

This commit is contained in:
Tommaso Stocchi
2026-03-27 11:08:04 +01:00
committed by GitHub
944 changed files with 34527 additions and 16150 deletions
+28 -27
View File
@@ -22,11 +22,10 @@
<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.1" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="2.0.0-beta.1" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0-beta.2" />
<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.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- Google Gemini -->
<PackageVersion Include="Google.GenAI" Version="0.11.0" />
@@ -43,12 +42,12 @@
<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" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.3" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.4" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.4" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.3" />
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.3" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.4" />
<PackageVersion Include="System.Text.Json" Version="10.0.4" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.4" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
@@ -67,25 +66,26 @@
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.3.0-preview.1.26109.11" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Compliance.Abstractions" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
<!-- Vector Stores -->
@@ -114,9 +114,9 @@
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="Microsoft.ML.Tokenizers" Version="2.0.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.8.0" />
<PackageVersion Include="OpenAI" Version="2.9.1" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.1" />
<!-- Workflows -->
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.2.4.1" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.4.1" />
@@ -152,6 +152,7 @@
<!-- Symbols -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<!-- Toolset -->
<PackageVersion Include="ReferenceTrimmer" Version="3.4.5" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
+8 -2
View File
@@ -67,6 +67,7 @@
<Project Path="samples/02-agents/Agents/Agent_Step16_Declarative/Agent_Step16_Declarative.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Agent_Step17_AdditionalAIContext.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Agent_Step19_InFunctionLoopCheckpointing.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
@@ -86,6 +87,8 @@
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
@@ -111,7 +114,7 @@
</Folder>
<Folder Name="/Samples/02-agents/AgentSkills/">
<File Path="samples/02-agents/AgentSkills/README.md" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_BasicSkills/Agent_Step01_BasicSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
@@ -319,7 +322,6 @@
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithTools/AgentWithTools.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj" />
</Folder>
@@ -463,6 +465,10 @@
<File Path="src/Shared/Samples/TextOutputHelperExtensions.cs" />
<File Path="src/Shared/Samples/XunitLogger.cs" />
</Folder>
<Folder Name="/Solution Items/src/Shared/Redaction/">
<File Path="src/Shared/Redaction/README.md" />
<File Path="src/Shared/Redaction/ReplacingRedactor.cs" />
</Folder>
<Folder Name="/Solution Items/src/Shared/Throw/">
<File Path="src/Shared/Throw/README.md" />
<File Path="src/Shared/Throw/Throw.cs" />
+3
View File
@@ -29,4 +29,7 @@
<ItemGroup Condition="'$(InjectSharedDiagnosticIds)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\DiagnosticIds\*.cs" LinkBase="Shared\DiagnosticIds" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedRedaction)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Redaction\*.cs" LinkBase="Shared\Redaction" />
</ItemGroup>
</Project>
@@ -59,14 +59,14 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
{
switch (content)
{
case FunctionApprovalRequestContent approvalRequest:
DisplayApprovalRequest(approvalRequest);
case ToolApprovalRequestContent approvalRequest when approvalRequest.ToolCall is FunctionCallContent fcc:
DisplayApprovalRequest(approvalRequest, fcc);
Console.Write($"\nApprove '{approvalRequest.FunctionCall.Name}'? (yes/no): ");
Console.Write($"\nApprove '{fcc.Name}'? (yes/no): ");
string? userInput = Console.ReadLine();
bool approved = userInput?.ToUpperInvariant() is "YES" or "Y";
FunctionApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved);
ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved);
if (approvalRequest.AdditionalProperties != null)
{
@@ -128,19 +128,19 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
}
#pragma warning disable MEAI001
static void DisplayApprovalRequest(FunctionApprovalRequestContent approvalRequest)
static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, FunctionCallContent fcc)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine();
Console.WriteLine("============================================================");
Console.WriteLine("APPROVAL REQUIRED");
Console.WriteLine("============================================================");
Console.WriteLine($"Function: {approvalRequest.FunctionCall.Name}");
Console.WriteLine($"Function: {fcc.Name}");
if (approvalRequest.FunctionCall.Arguments != null)
if (fcc.Arguments != null)
{
Console.WriteLine("Arguments:");
foreach (var arg in approvalRequest.FunctionCall.Arguments)
foreach (var arg in fcc.Arguments)
{
Console.WriteLine($" {arg.Key} = {arg.Value}");
}
@@ -9,7 +9,7 @@ using ServerFunctionApproval;
/// <summary>
/// A delegating agent that handles server function approval requests and responses.
/// Transforms between FunctionApprovalRequestContent/FunctionApprovalResponseContent
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
/// and the server's request_approval tool call pattern.
/// </summary>
internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
@@ -50,14 +50,14 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
}
#pragma warning disable MEAI001 // Type is for evaluation purposes only
private static FunctionResultContent ConvertApprovalResponseToToolResult(FunctionApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions)
private static FunctionResultContent ConvertApprovalResponseToToolResult(ToolApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions)
{
return new FunctionResultContent(
callId: approvalResponse.Id,
callId: approvalResponse.RequestId,
result: JsonSerializer.SerializeToElement(
new ApprovalResponse
{
ApprovalId = approvalResponse.Id,
ApprovalId = approvalResponse.RequestId,
Approved = approvalResponse.Approved
},
jsonOptions));
@@ -89,7 +89,7 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
{
List<ChatMessage>? result = null;
Dictionary<string, FunctionApprovalRequestContent> approvalRequests = [];
Dictionary<string, ToolApprovalRequestContent> approvalRequests = [];
for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++)
{
var message = messages[messageIndex];
@@ -102,21 +102,21 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
var content = message.Contents[contentIndex];
// Handle pending approval requests (transform to tool call)
if (content is FunctionApprovalRequestContent approvalRequest &&
if (content is ToolApprovalRequestContent approvalRequest &&
approvalRequest.AdditionalProperties?.TryGetValue("original_function", out var originalFunction) == true &&
originalFunction is FunctionCallContent original)
{
approvalRequests[approvalRequest.Id] = approvalRequest;
approvalRequests[approvalRequest.RequestId] = approvalRequest;
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
transformedContents.Add(original);
}
// Handle pending approval responses (transform to tool result)
else if (content is FunctionApprovalResponseContent approvalResponse &&
approvalRequests.TryGetValue(approvalResponse.Id, out var correspondingRequest))
else if (content is ToolApprovalResponseContent approvalResponse &&
approvalRequests.TryGetValue(approvalResponse.RequestId, out var correspondingRequest))
{
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
transformedContents.Add(ConvertApprovalResponseToToolResult(approvalResponse, jsonSerializerOptions));
approvalRequests.Remove(approvalResponse.Id);
approvalRequests.Remove(approvalResponse.RequestId);
correspondingRequest.AdditionalProperties?.Remove("original_function");
}
// Skip historical approval content
@@ -198,8 +198,8 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
var functionCallArgs = (Dictionary<string, object?>?)approvalRequest.FunctionArguments?
.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
var approvalRequestContent = new FunctionApprovalRequestContent(
id: approvalRequest.ApprovalId,
var approvalRequestContent = new ToolApprovalRequestContent(
requestId: approvalRequest.ApprovalId,
new FunctionCallContent(
callId: approvalRequest.ApprovalId,
name: approvalRequest.FunctionName,
@@ -9,7 +9,7 @@ using ServerFunctionApproval;
/// <summary>
/// A delegating agent that handles function approval requests on the server side.
/// Transforms between FunctionApprovalRequestContent/FunctionApprovalResponseContent
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
/// and the request_approval tool call pattern for client communication.
/// </summary>
internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
@@ -50,7 +50,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
}
#pragma warning disable MEAI001 // Type is for evaluation purposes only
private static FunctionApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions)
private static ToolApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions)
{
if (toolCall.Name != "request_approval" || toolCall.Arguments == null)
{
@@ -67,15 +67,15 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
throw new InvalidOperationException("Failed to deserialize approval request from tool call");
}
return new FunctionApprovalRequestContent(
id: request.ApprovalId,
return new ToolApprovalRequestContent(
requestId: request.ApprovalId,
new FunctionCallContent(
callId: request.ApprovalId,
name: request.FunctionName,
arguments: request.FunctionArguments));
}
private static FunctionApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, FunctionApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
{
var approvalResponse = result.Result is JsonElement je ?
(ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
@@ -121,7 +121,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
// Track approval ID to original call ID mapping
_ = new Dictionary<string, string>();
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Dictionary<string, FunctionApprovalRequestContent> trackedRequestApprovalToolCalls = new(); // Remote approvals
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = new(); // Remote approvals
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
{
var message = messages[messageIndex];
@@ -181,11 +181,10 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
{
var content = update.Contents[i];
#pragma warning disable MEAI001 // Type is for evaluation purposes only
if (content is FunctionApprovalRequestContent request)
if (content is ToolApprovalRequestContent request && request.ToolCall is FunctionCallContent functionCall)
{
updatedContents ??= [.. update.Contents];
var functionCall = request.FunctionCall;
var approvalId = request.Id;
var approvalId = request.RequestId;
var approvalData = new ApprovalRequest
{
@@ -18,6 +18,7 @@ using OpenTelemetry.Trace;
#region Setup Telemetry
// Source name for this sample's custom ActivitySource and Meter; other instrumentation uses their own sources/categories.
const string SourceName = "OpenTelemetryAspire.ConsoleApp";
const string ServiceName = "AgentOpenTelemetry";
@@ -40,7 +41,6 @@ var resource = ResourceBuilder.CreateDefault()
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
.AddSource(SourceName) // Our custom activity source
.AddSource("*Microsoft.Agents.AI") // Agent Framework telemetry
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
@@ -54,8 +54,7 @@ using var tracerProvider = tracerProviderBuilder.Build();
// Setup metrics with resource and instrument name filtering
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
.AddMeter(SourceName) // Our custom meter
.AddMeter("*Microsoft.Agents.AI") // Agent Framework metrics
.AddMeter(SourceName) // Our custom meter source
.AddHttpClientInstrumentation() // HTTP client metrics
.AddRuntimeInstrumentation() // .NET runtime metrics
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
@@ -128,7 +127,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
instructions: "You are a helpful assistant that provides concise and informative responses.",
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
.AsBuilder()
.UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.Build();
var session = await agent.CreateSessionAsync();
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CS0618 // Type or member is obsolete - sample uses deprecated PersistentAgentsClientExtensions
// This sample shows how to create and use a simple AI agent with Azure Foundry Agents as the backend.
using Azure.AI.Agents.Persistent;
@@ -3,7 +3,7 @@
// This sample shows how to create and use a AI agents with Azure Foundry Agents as the backend.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -17,8 +17,8 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
.GetResponsesClient()
.AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -29,8 +29,8 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
AIAgent agentStoreFalse = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.AsIChatClientWithStoredOutputDisabled()
.GetResponsesClient()
.AsIChatClientWithStoredOutputDisabled(model: deploymentName)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
</ItemGroup>
</Project>
@@ -11,8 +11,8 @@ var model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt
AIAgent agent = new OpenAIClient(
apiKey)
.GetResponsesClient(model)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
.GetResponsesClient()
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -1,49 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use Agent Skills with a ChatClientAgent.
// Agent Skills are modular packages of instructions and resources that extend an agent's capabilities.
// Skills follow the progressive disclosure pattern: advertise -> load -> read resources.
//
// This sample includes the expense-report skill:
// - Policy-based expense filing with references and assets
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";
// --- Skills Provider ---
// Discovers skills from the 'skills' directory and makes them available to the agent
var skillsProvider = new FileAgentSkillsProvider(skillPath: Path.Combine(AppContext.BaseDirectory, "skills"));
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.AsAIAgent(new ChatClientAgentOptions
{
Name = "SkillsAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant.",
},
AIContextProviders = [skillsProvider],
});
// --- Example 1: Expense policy question (loads FAQ resource) ---
Console.WriteLine("Example 1: Checking expense policy FAQ");
Console.WriteLine("---------------------------------------");
AgentResponse response1 = await agent.RunAsync("Are tips reimbursable? I left a 25% tip on a taxi ride and want to know if that's covered.");
Console.WriteLine($"Agent: {response1.Text}\n");
// --- Example 2: Filing an expense report (multi-turn with template asset) ---
Console.WriteLine("Example 2: Filing an expense report");
Console.WriteLine("---------------------------------------");
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response2 = await agent.RunAsync("I had 3 client dinners and a $1,200 flight last week. Return a draft expense report and ask about any missing details.",
session);
Console.WriteLine($"Agent: {response2.Text}\n");
@@ -1,63 +0,0 @@
# Agent Skills Sample
This sample demonstrates how to use **Agent Skills** with a `ChatClientAgent` in the Microsoft Agent Framework.
## What are Agent Skills?
Agent Skills are modular packages of instructions and resources that enable AI agents to perform specialized tasks. They follow the [Agent Skills specification](https://agentskills.io/) and implement the progressive disclosure pattern:
1. **Advertise**: Skills are advertised with name + description (~100 tokens per skill)
2. **Load**: Full instructions are loaded on-demand via `load_skill` tool
3. **Resources**: References and other files loaded via `read_skill_resource` tool
## Skills Included
### expense-report
Policy-based expense filing with spending limits, receipt requirements, and approval workflows.
- `references/POLICY_FAQ.md` — Detailed expense policy Q&A
- `assets/expense-report-template.md` — Submission template
## Project Structure
```
Agent_Step01_BasicSkills/
├── Program.cs
├── Agent_Step01_BasicSkills.csproj
└── skills/
└── expense-report/
├── SKILL.md
├── references/
│ └── POLICY_FAQ.md
└── assets/
└── expense-report-template.md
```
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
### Setup
1. Set environment variables:
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```
2. Run the sample:
```bash
dotnet run
```
### Examples
The sample runs two examples:
1. **Expense policy FAQ** — Asks about tip reimbursement; the agent loads the expense-report skill and reads the FAQ resource
2. **Filing an expense report** — Multi-turn conversation to draft an expense report using the template asset
## Learn More
- [Agent Skills Specification](https://agentskills.io/)
- [Microsoft Agent Framework Documentation](../../../../../docs/)
@@ -1,40 +0,0 @@
---
name: expense-report
description: File and validate employee expense reports according to Contoso company policy. Use when asked about expense submissions, reimbursement rules, receipt requirements, spending limits, or expense categories.
metadata:
author: contoso-finance
version: "2.1"
---
# Expense Report
## Categories and Limits
| Category | Limit | Receipt | Approval |
|---|---|---|---|
| Meals — solo | $50/day | >$25 | No |
| Meals — team/client | $75/person | Always | Manager if >$200 total |
| Lodging | $250/night | Always | Manager if >3 nights |
| Ground transport | $100/day | >$15 | No |
| Airfare | Economy | Always | Manager; VP if >$1,500 |
| Conference/training | $2,000/event | Always | Manager + L&D |
| Office supplies | $100 | Yes | No |
| Software/subscriptions | $50/month | Yes | Manager if >$200/year |
## Filing Process
1. Collect receipts — must show vendor, date, amount, payment method.
2. Categorize per table above.
3. Use template: [assets/expense-report-template.md](assets/expense-report-template.md).
4. For client/team meals: list attendee names and business purpose.
5. Submit — auto-approved if <$500; manager if $500$2,000; VP if >$2,000.
6. Reimbursement: 10 business days via direct deposit.
## Policy Rules
- Submit within 30 days of transaction.
- Alcohol is never reimbursable.
- Foreign currency: convert to USD at transaction-date rate; note original currency and amount.
- Mixed personal/business travel: only business portion reimbursable; provide comparison quotes.
- Lost receipts (>$25): file Lost Receipt Affidavit from Finance. Max 2 per quarter.
- For policy questions not covered above, consult the FAQ: [references/POLICY_FAQ.md](references/POLICY_FAQ.md). Answers should be based on what this document and the FAQ state.
@@ -1,5 +0,0 @@
# Expense Report Template
| Date | Category | Vendor | Description | Amount (USD) | Original Currency | Original Amount | Attendees | Business Purpose | Receipt Attached |
|------|----------|--------|-------------|--------------|-------------------|-----------------|-----------|------------------|------------------|
| | | | | | | | | | Yes or No |
@@ -1,55 +0,0 @@
# Expense Policy — Frequently Asked Questions
## Meals
**Q: Can I expense coffee or snacks during the workday?**
A: Daily coffee/snacks under $10 are not reimbursable (considered personal). Coffee purchased during a client meeting or team working session is reimbursable as a team meal.
**Q: What if a team dinner exceeds the per-person limit?**
A: The $75/person limit applies as a guideline. Overages up to 20% are accepted with a written justification (e.g., "client dinner at venue chosen by client"). Overages beyond 20% require pre-approval from your VP.
**Q: Do I need to list every attendee?**
A: Yes. For client meals, list the client's name and company. For team meals, list all employee names. For groups over 10, you may attach a separate attendee list.
## Travel
**Q: Can I book a premium economy or business class flight?**
A: Economy class is the standard. Premium economy is allowed for flights over 6 hours. Business class requires VP pre-approval and is generally reserved for flights over 10 hours or medical accommodation.
**Q: What about ride-sharing (Uber/Lyft) vs. rental cars?**
A: Use ride-sharing for trips under 30 miles round-trip. Rent a car for multi-day travel or when ride-sharing would exceed $100/day. Always choose the compact/standard category unless traveling with 3+ people.
**Q: Are tips reimbursable?**
A: Tips up to 20% are reimbursable for meals, taxi/ride-share, and hotel housekeeping. Tips above 20% require justification.
## Lodging
**Q: What if the $250/night limit isn't enough for the city I'm visiting?**
A: For high-cost cities (New York, San Francisco, London, Tokyo, Sydney), the limit is automatically increased to $350/night. No additional approval is needed. For other locations where rates are unusually high (e.g., during a major conference), request a per-trip exception from your manager before booking.
**Q: Can I stay with friends/family instead and get a per-diem?**
A: No. Contoso reimburses actual lodging costs only, not per-diems.
## Subscriptions and Software
**Q: Can I expense a personal productivity tool?**
A: Software must be directly related to your job function. Tools like IDE licenses, design software, or project management apps are reimbursable. General productivity apps (note-taking, personal calendar) are not, unless your manager confirms a business need in writing.
**Q: What about annual subscriptions?**
A: Annual subscriptions over $200 require manager approval before purchase. Submit the approval email with your expense report.
## Receipts and Documentation
**Q: My receipt is faded/damaged. What do I do?**
A: Try to obtain a duplicate from the vendor. If not possible, submit a Lost Receipt Affidavit (available from the Finance SharePoint site). You're limited to 2 affidavits per quarter.
**Q: Do I need a receipt for parking meters or tolls?**
A: For amounts under $15, no receipt is required — just note the date, location, and amount. For $15 and above, a receipt or bank/credit card statement excerpt is required.
## Approval and Reimbursement
**Q: My manager is on leave. Who approves my report?**
A: Expense reports can be approved by your skip-level manager or any manager designated as an alternate approver in the expense system.
**Q: Can I submit expenses from a previous quarter?**
A: The standard 30-day window applies. Expenses older than 30 days require a written explanation and VP approval. Expenses older than 90 days are not reimbursable except in extraordinary circumstances (extended leave, medical emergency) with CFO approval.
@@ -14,6 +14,10 @@
<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>
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use file-based Agent Skills with a ChatClientAgent.
// Skills are discovered from SKILL.md files on disk and follow the progressive disclosure pattern:
// 1. Advertise — skill names and descriptions in the system prompt
// 2. Load — full instructions loaded on demand via load_skill tool
// 3. Read resources — reference files read via read_skill_resource tool
// 4. Run scripts — scripts executed via run_skill_script tool with a subprocess executor
//
// This sample uses a unit-converter skill that converts between miles, kilometers, pounds, and kilograms.
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";
// --- Skills Provider ---
// Discovers skills from the 'skills' directory containing SKILL.md files.
// The script runner runs file-based scripts (e.g. Python) as local subprocesses.
var skillsProvider = new AgentSkillsProvider(
Path.Combine(AppContext.BaseDirectory, "skills"),
SubprocessScriptRunner.RunAsync);
// --- 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 file-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}");
@@ -0,0 +1,51 @@
# File-Based Agent Skills Sample
This sample demonstrates how to use **file-based Agent Skills** with a `ChatClientAgent`.
## What it demonstrates
- 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
- Running file-based scripts (Python) via a subprocess-based executor
## Skills Included
### unit-converter
Converts between common units (miles↔km, pounds↔kg) using a multiplication factor.
- `references/conversion-table.md` — Conversion factor table
- `scripts/convert.py` — Python script that performs the conversion
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
- Python 3 installed and available as `python3` on your PATH
### 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 file-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,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/conversion-table.md` to find the correct factor
2. Run the `scripts/convert.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.py --value 26.2 --factor 1.60934
# python scripts/convert.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.py --value 26.2 --factor 1.60934\n"
" python scripts/convert.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()
@@ -4,4 +4,4 @@ Samples demonstrating Agent Skills capabilities.
| Sample | Description |
|--------|-------------|
| [Agent_Step01_BasicSkills](Agent_Step01_BasicSkills/) | Using Agent Skills with a ChatClientAgent, including progressive disclosure and skill resources |
| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. |
@@ -0,0 +1,137 @@
// Copyright (c) Microsoft. All rights reserved.
// Sample subprocess-based skill script runner.
// Executes file-based skill scripts as local subprocesses.
// This is provided for demonstration purposes only.
using System.Diagnostics;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
/// <summary>
/// Executes file-based skill scripts as local subprocesses.
/// </summary>
/// <remarks>
/// This runner uses the script's absolute path, converts the arguments
/// to CLI flags, and returns captured output. It is intended for
/// demonstration purposes only.
/// </remarks>
internal static class SubprocessScriptRunner
{
/// <summary>
/// Runs a skill script as a local subprocess.
/// </summary>
public static async Task<object?> RunAsync(
AgentFileSkill skill,
AgentFileSkillScript script,
AIFunctionArguments arguments,
CancellationToken cancellationToken)
{
if (!File.Exists(script.FullPath))
{
return $"Error: Script file not found: {script.FullPath}";
}
string extension = Path.GetExtension(script.FullPath);
string? interpreter = extension switch
{
".py" => "python3",
".js" => "node",
".sh" => "bash",
".ps1" => "pwsh",
_ => null,
};
var startInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(script.FullPath) ?? ".",
};
if (interpreter is not null)
{
startInfo.FileName = interpreter;
startInfo.ArgumentList.Add(script.FullPath);
}
else
{
startInfo.FileName = script.FullPath;
}
if (arguments is not null)
{
foreach (var (key, value) in arguments)
{
if (value is bool boolValue)
{
if (boolValue)
{
startInfo.ArgumentList.Add(NormalizeKey(key));
}
}
else if (value is not null)
{
startInfo.ArgumentList.Add(NormalizeKey(key));
startInfo.ArgumentList.Add(value.ToString()!);
}
}
}
Process? process = null;
try
{
process = Process.Start(startInfo);
if (process is null)
{
return $"Error: Failed to start process for script '{script.Name}'.";
}
Task<string> outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
Task<string> errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
string output = await outputTask.ConfigureAwait(false);
string error = await errorTask.ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
output += $"\nStderr:\n{error}";
}
if (process.ExitCode != 0)
{
output += $"\nScript exited with code {process.ExitCode}";
}
return string.IsNullOrEmpty(output) ? "(no output)" : output.Trim();
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Kill the process on cancellation to avoid leaving orphaned subprocesses.
process?.Kill(entireProcessTree: true);
throw;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
return $"Error: Failed to execute script '{script.Name}': {ex.Message}";
}
finally
{
process?.Dispose();
}
}
/// <summary>
/// Normalizes a parameter key to a consistent --flag format.
/// Models may return keys with or without leading dashes (e.g., "value" vs "--value").
/// </summary>
private static string NormalizeKey(string key) => "--" + key.TrimStart('-');
}
@@ -10,8 +10,8 @@ var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new I
var model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-5";
var client = new OpenAIClient(apiKey)
.GetResponsesClient(model)
.AsIChatClient().AsBuilder()
.GetResponsesClient()
.AsIChatClient(model).AsBuilder()
.ConfigureOptions(o =>
{
o.Reasoning = new()
@@ -20,19 +20,21 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
/// <param name="instructions">Optional instructions for the agent.</param>
/// <param name="name">Optional name for the agent.</param>
/// <param name="description">Optional description for the agent.</param>
/// <param name="model">Optional default model ID to use for requests. Required when using a plain <see cref="ResponsesClient"/> (not via Azure OpenAI).</param>
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
public OpenAIResponseClientAgent(
ResponsesClient client,
string? instructions = null,
string? name = null,
string? description = null,
string? model = null,
ILoggerFactory? loggerFactory = null) :
this(client, new()
{
Name = name,
Description = description,
ChatOptions = new ChatOptions() { Instructions = instructions },
}, loggerFactory)
}, model, loggerFactory)
{
}
@@ -41,10 +43,11 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
/// </summary>
/// <param name="client">Instance of <see cref="ResponsesClient"/></param>
/// <param name="options">Options to create the agent.</param>
/// <param name="model">Optional default model ID to use for requests. Required when using a plain <see cref="ResponsesClient"/> (not via Azure OpenAI).</param>
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
public OpenAIResponseClientAgent(
ResponsesClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) :
base(new ChatClientAgent((client ?? throw new ArgumentNullException(nameof(client))).AsIChatClient(), options, loggerFactory))
ResponsesClient client, ChatClientAgentOptions options, string? model = null, ILoggerFactory? loggerFactory = null) :
base(new ChatClientAgent((client ?? throw new ArgumentNullException(nameof(client))).AsIChatClient(model), options, loggerFactory))
{
}
@@ -10,10 +10,10 @@ var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new I
var model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-4o-mini";
// Create a ResponsesClient directly from OpenAIClient
ResponsesClient responseClient = new OpenAIClient(apiKey).GetResponsesClient(model);
ResponsesClient responseClient = new OpenAIClient(apiKey).GetResponsesClient();
// Create an agent directly from the ResponsesClient using OpenAIResponseClientAgent
OpenAIResponseClientAgent agent = new(responseClient, instructions: "You are good at telling jokes.", name: "Joker");
OpenAIResponseClientAgent agent = new(responseClient, instructions: "You are good at telling jokes.", name: "Joker", model: model);
ResponseItem userMessage = ResponseItem.CreateUserMessageItem("Tell me a joke about a pirate.");
@@ -22,7 +22,7 @@ OpenAIClient openAIClient = new(apiKey);
ConversationClient conversationClient = openAIClient.GetConversationClient();
// Create an agent directly from the ResponsesClient using OpenAIResponseClientAgent
ChatClientAgent agent = new(openAIClient.GetResponsesClient(model).AsIChatClient(), instructions: "You are a helpful assistant.", name: "ConversationAgent");
ChatClientAgent agent = new(openAIClient.GetResponsesClient().AsIChatClient(model), instructions: "You are a helpful assistant.", name: "ConversationAgent");
ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}")));
@@ -73,16 +73,28 @@ foreach (ClientResult result in getConversationItemsResults.GetRawPages())
using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString());
foreach (JsonElement element in getConversationItemsResultAsJson.RootElement.GetProperty("data").EnumerateArray())
{
// Skip non-message items (e.g. tool calls, reasoning) that lack a "role" property
if (!element.TryGetProperty("role"u8, out var roleElement))
{
continue;
}
string messageId = element.GetProperty("id"u8).ToString();
string messageRole = element.GetProperty("role"u8).ToString();
string messageRole = roleElement.ToString();
Console.WriteLine($" Message ID: {messageId}");
Console.WriteLine($" Message Role: {messageRole}");
foreach (var content in element.GetProperty("content").EnumerateArray())
if (element.TryGetProperty("content"u8, out var contentElement))
{
string messageContentText = content.GetProperty("text"u8).ToString();
Console.WriteLine($" Message Text: {messageContentText}");
foreach (var content in contentElement.EnumerateArray())
{
if (content.TryGetProperty("text"u8, out var textElement))
{
Console.WriteLine($" Message Text: {textElement}");
}
}
}
Console.WriteLine();
}
}
@@ -36,11 +36,11 @@ AIAgent agent = new AzureOpenAIClient(
// For simplicity, we are assuming here that only function approvals are pending.
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
// For streaming use:
// var updates = await agent.RunStreamingAsync("What is the weather like in Amsterdam?", session).ToListAsync();
// approvalRequests = updates.SelectMany(x => x.Contents).OfType<FunctionApprovalRequestContent>().ToList();
// approvalRequests = updates.SelectMany(x => x.Contents).OfType<ToolApprovalRequestContent>().ToList();
while (approvalRequests.Count > 0)
{
@@ -48,18 +48,18 @@ while (approvalRequests.Count > 0)
List<ChatMessage> userInputResponses = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {((FunctionCallContent)functionApprovalRequest.ToolCall).Name}");
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
});
// Pass the user input responses back to the agent for further processing.
response = await agent.RunAsync(userInputResponses, session);
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
// For streaming use:
// updates = await agent.RunStreamingAsync(userInputResponses, session).ToListAsync();
// approvalRequests = updates.SelectMany(x => x.Contents).OfType<FunctionApprovalRequestContent>().ToList();
// approvalRequests = updates.SelectMany(x => x.Contents).OfType<ToolApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
@@ -10,14 +10,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -2,7 +2,7 @@
// This sample shows how to expose an AI agent as an MCP tool.
using Azure.AI.Agents.Persistent;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -15,18 +15,15 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYME
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new DefaultAzureCredential());
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// Create a server side persistent agent
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
// Create a server side agent and expose it as an AIAgent.
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
instructions: "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.",
name: "Joker",
description: "An agent that tells jokes.");
// Retrieve the server side persistent agent as an AIAgent.
AIAgent agent = await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
// Convert the agent to an AIFunction and then to an MCP tool.
// The agent name and description will be used as the mcp tool name and description.
McpServerTool tool = McpServerTool.Create(agent.AsAIFunction());
@@ -16,5 +16,11 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\walkway.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -22,7 +22,7 @@ var agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential(
ChatMessage message = new(ChatRole.User, [
new TextContent("What do you see in this image?"),
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
await DataContent.LoadFromAsync("Assets/walkway.jpg"),
]);
var session = await agent.CreateSessionAsync();
@@ -25,8 +25,9 @@ var stateStore = new Dictionary<string, JsonElement?>();
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.GetResponsesClient()
.AsAIAgent(
model: deploymentName,
name: "SpaceNovelWriter",
instructions: "You are a space novel writer. Always research relevant facts and generate character profiles for the main characters before writing novels." +
"Write complete chapters without asking for approval or feedback. Do not ask the user about tone, style, pace, or format preferences - just write the novel based on the request.",
@@ -246,7 +246,7 @@ async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMes
AgentResponse response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
// For simplicity, we are assuming here that only function approvals are pending.
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
while (approvalRequests.Count > 0)
{
@@ -255,13 +255,13 @@ async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMes
response.Messages = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {((FunctionCallContent)functionApprovalRequest.ToolCall).Name}");
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
});
response = await innerAgent.RunAsync(response.Messages, session, options, cancellationToken);
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
}
return response;
@@ -16,8 +16,8 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.AsAIAgent();
.GetResponsesClient()
.AsAIAgent(model: deploymentName);
// Enable background responses (only supported by OpenAI Responses at this time).
AgentRunOptions options = new() { AllowBackgroundResponses = true };
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CS0618 // Type or member is obsolete - sample uses deprecated PersistentAgentsClientExtensions
// This sample shows how to create an Azure AI Foundry Agent with the Deep Research Tool.
using Azure.AI.Agents.Persistent;
@@ -12,13 +12,9 @@
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Agents.ObjectModel" />
<PackageReference Include="Microsoft.Agents.ObjectModel.Json" />
<PackageReference Include="Microsoft.Agents.ObjectModel.PowerFx" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Declarative\Microsoft.Agents.AI.Declarative.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,226 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how the ChatClientAgent persists chat history after each individual
// call to the AI service.
// When an agent uses tools, FunctionInvokingChatClient may loop multiple times
// (service call → tool execution → service call), and intermediate messages (tool calls and
// results) are persisted after each service call. This allows you to inspect or recover them
// even if the process is interrupted mid-loop, but may also result in chat history that is not
// yet finalized (e.g., tool calls without results) being persisted, which may be undesirable in some cases.
//
// To opt into end-of-run persistence instead (atomic run semantics), set
// PersistChatHistoryAtEndOfRun = true on ChatClientAgentOptions.
//
// The sample runs two multi-turn conversations: one using non-streaming (RunAsync) and one
// using streaming (RunStreamingAsync), to demonstrate correct behavior in both modes.
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var store = Environment.GetEnvironmentVariable("AZURE_OPENAI_RESPONSES_STORE") ?? "false";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AzureOpenAIClient openAIClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Define multiple tools so the model makes several tool calls in a single run.
[Description("Get the current weather for a city.")]
static string GetWeather([Description("The city name.")] string city) =>
city.ToUpperInvariant() switch
{
"SEATTLE" => "Seattle: 55°F, cloudy with light rain.",
"NEW YORK" => "New York: 72°F, sunny and warm.",
"LONDON" => "London: 48°F, overcast with fog.",
"DUBLIN" => "Dublin: 43°F, overcast with fog.",
_ => $"{city}: weather data not available."
};
[Description("Get the current time in a city.")]
static string GetTime([Description("The city name.")] string city) =>
city.ToUpperInvariant() switch
{
"SEATTLE" => "Seattle: 9:00 AM PST",
"NEW YORK" => "New York: 12:00 PM EST",
"LONDON" => "London: 5:00 PM GMT",
"DUBLIN" => "Dublin: 5:00 PM GMT",
_ => $"{city}: time data not available."
};
// Create the agent — per-service-call persistence is the default behavior.
// The in-memory ChatHistoryProvider is used by default when the service does not require service stored chat
// history, so for those cases, we can inspect the chat history via session.TryGetInMemoryChatHistory().
IChatClient chatClient = string.Equals(store, "TRUE", StringComparison.OrdinalIgnoreCase) ?
openAIClient.GetResponsesClient().AsIChatClient(deploymentName) :
openAIClient.GetResponsesClient().AsIChatClientWithStoredOutputDisabled(deploymentName);
AIAgent agent = chatClient.AsAIAgent(
new ChatClientAgentOptions
{
Name = "WeatherAssistant",
ChatOptions = new()
{
Instructions = "You are a helpful assistant. When asked about multiple cities, call the appropriate tool for each city.",
Tools = [AIFunctionFactory.Create(GetWeather), AIFunctionFactory.Create(GetTime)]
},
});
await RunNonStreamingAsync();
await RunStreamingAsync();
async Task RunNonStreamingAsync()
{
int lastChatHistorySize = 0;
string lastConversationId = string.Empty;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("\n=== Non-Streaming Mode ===");
Console.ResetColor();
AgentSession session = await agent.CreateSessionAsync();
// First turn — ask about multiple cities so the model calls tools.
const string Prompt = "What's the weather and time in Seattle, New York, and London?";
PrintUserMessage(Prompt);
var response = await agent.RunAsync(Prompt, session);
PrintAgentResponse(response.Text);
PrintChatHistory(session, "After run", ref lastChatHistorySize, ref lastConversationId);
// Second turn — follow-up to verify chat history is correct.
const string FollowUp1 = "And Dublin?";
PrintUserMessage(FollowUp1);
response = await agent.RunAsync(FollowUp1, session);
PrintAgentResponse(response.Text);
PrintChatHistory(session, "After second run", ref lastChatHistorySize, ref lastConversationId);
// Third turn — follow-up to verify chat history is correct.
const string FollowUp2 = "Which city is the warmest?";
PrintUserMessage(FollowUp2);
response = await agent.RunAsync(FollowUp2, session);
PrintAgentResponse(response.Text);
PrintChatHistory(session, "After third run", ref lastChatHistorySize, ref lastConversationId);
}
async Task RunStreamingAsync()
{
int lastChatHistorySize = 0;
string lastConversationId = string.Empty;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("\n=== Streaming Mode ===");
Console.ResetColor();
AgentSession session = await agent.CreateSessionAsync();
// First turn — ask about multiple cities so the model calls tools.
const string Prompt = "What's the weather and time in Seattle, New York, and London?";
PrintUserMessage(Prompt);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("\n[Agent] ");
Console.ResetColor();
await foreach (var update in agent.RunStreamingAsync(Prompt, session))
{
Console.Write(update);
// During streaming we should be able to see updates to the chat history
// before the full run completes, as each service call is made and persisted.
PrintChatHistory(session, "During run", ref lastChatHistorySize, ref lastConversationId);
}
Console.WriteLine();
PrintChatHistory(session, "After run", ref lastChatHistorySize, ref lastConversationId);
// Second turn — follow-up to verify chat history is correct.
const string FollowUp1 = "And Dublin?";
PrintUserMessage(FollowUp1);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("\n[Agent] ");
Console.ResetColor();
await foreach (var update in agent.RunStreamingAsync(FollowUp1, session))
{
Console.Write(update);
// During streaming we should be able to see updates to the chat history
// before the full run completes, as each service call is made and persisted.
PrintChatHistory(session, "During second run", ref lastChatHistorySize, ref lastConversationId);
}
Console.WriteLine();
PrintChatHistory(session, "After second run", ref lastChatHistorySize, ref lastConversationId);
// Third turn — follow-up to verify chat history is correct.
const string FollowUp2 = "Which city is the warmest?";
PrintUserMessage(FollowUp2);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("\n[Agent] ");
Console.ResetColor();
await foreach (var update in agent.RunStreamingAsync(FollowUp2, session))
{
Console.Write(update);
// During streaming we should be able to see updates to the chat history
// before the full run completes, as each service call is made and persisted.
PrintChatHistory(session, "During third run", ref lastChatHistorySize, ref lastConversationId);
}
Console.WriteLine();
PrintChatHistory(session, "After third run", ref lastChatHistorySize, ref lastConversationId);
}
void PrintUserMessage(string message)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("\n[User] ");
Console.ResetColor();
Console.WriteLine(message);
}
void PrintAgentResponse(string? text)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("\n[Agent] ");
Console.ResetColor();
Console.WriteLine(text);
}
// Helper to print the current chat history from the session.
void PrintChatHistory(AgentSession session, string label, ref int lastChatHistorySize, ref string lastConversationId)
{
if (session.TryGetInMemoryChatHistory(out var history) && history.Count != lastChatHistorySize)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"\n [{label} — Chat history: {history.Count} message(s)]");
foreach (var msg in history)
{
var preview = msg.Text?.Length > 80 ? msg.Text[..80] + "…" : msg.Text;
var contentTypes = string.Join(", ", msg.Contents.Select(c => c.GetType().Name));
Console.WriteLine($" {msg.Role,-12} | {(string.IsNullOrWhiteSpace(preview) ? $"[{contentTypes}]" : preview)}");
}
Console.ResetColor();
lastChatHistorySize = history.Count;
}
if (session is ChatClientAgentSession ccaSession && ccaSession.ConversationId is not null && ccaSession.ConversationId != lastConversationId)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [{label} — Conversation ID: {ccaSession.ConversationId}]");
Console.ResetColor();
lastConversationId = ccaSession.ConversationId;
}
}
@@ -0,0 +1,63 @@
# In-Function-Loop Checkpointing
This sample demonstrates how `ChatClientAgent` persists chat history after each individual call to the AI service by default. This per-service-call persistence ensures intermediate progress is saved during the function invocation loop.
## What This Sample Shows
When an agent uses tools, the `FunctionInvokingChatClient` loops multiple times (service call → tool execution → service call → …). By default, chat history is persisted after each service call via the `ChatHistoryPersistingChatClient` decorator:
- A `ChatHistoryPersistingChatClient` decorator is automatically inserted into the chat client pipeline
- After each service call, the decorator notifies the `ChatHistoryProvider` (and any `AIContextProvider` instances) with the new messages
- Only **new** messages are sent to providers on each notification — messages that were already persisted in an earlier call within the same run are deduplicated automatically
To opt into end-of-run persistence instead (atomic run semantics), set `PersistChatHistoryAtEndOfRun = true` on `ChatClientAgentOptions`. In that mode, the decorator marks messages with metadata rather than persisting them immediately, and `ChatClientAgent` persists only the marked messages at the end of the run.
Per-service-call persistence is useful for:
- **Crash recovery** — if the process is interrupted mid-loop, the intermediate tool calls and results are already persisted
- **Observability** — you can inspect the chat history while the agent is still running (e.g., during streaming)
- **Long-running tool loops** — agents with many sequential tool calls benefit from incremental persistence
## How It Works
The sample asks the agent about the weather and time in three cities. The model calls the `GetWeather` and `GetTime` tools for each city, resulting in multiple service calls within a single `RunStreamingAsync` invocation. After the run completes, the sample prints the full chat history to show all the intermediate messages that were persisted along the way.
### Pipeline Architecture
```
ChatClientAgent
└─ FunctionInvokingChatClient (handles tool call loop)
└─ ChatHistoryPersistingChatClient (persists after each service call)
└─ Leaf IChatClient (Azure OpenAI)
```
## Prerequisites
- .NET 10 SDK or later
- Azure OpenAI service endpoint and model deployment
- Azure CLI installed and authenticated
**Note**: This sample uses `DefaultAzureCredential`. Sign in with `az login` before running. For production, prefer a specific credential such as `ManagedIdentityCredential`. For more information, see the [Azure CLI authentication documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
## Environment Variables
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Required
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
## Running the Sample
```powershell
cd dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing
dotnet run
```
## Expected Behavior
The sample runs two conversation turns:
1. **First turn** — asks about weather and time in three cities. The model calls `GetWeather` and `GetTime` tools (potentially in parallel or sequentially), then provides a summary. The chat history dump after the run shows all the intermediate tool call and result messages.
2. **Second turn** — asks a follow-up question ("Which city is the warmest?") that uses the persisted conversation context. The chat history dump shows the full accumulated conversation.
The chat history printout uses `session.TryGetInMemoryChatHistory()` to inspect the in-memory storage.
@@ -45,6 +45,7 @@ Before you begin, ensure you have the following prerequisites:
|[Declarative agent](./Agent_Step16_Declarative/)|This sample demonstrates how to declaratively define an agent.|
|[Providing additional AI Context to an agent using multiple AIContextProviders](./Agent_Step17_AdditionalAIContext/)|This sample demonstrates how to inject additional AI context into a ChatClientAgent using multiple custom AIContextProvider components that are attached to the agent.|
|[Using compaction pipeline with an agent](./Agent_Step18_CompactionPipeline/)|This sample demonstrates how to use a compaction pipeline to efficiently limit the size of the conversation history for an agent.|
|[In-function-loop checkpointing](./Agent_Step19_InFunctionLoopCheckpointing/)|This sample demonstrates how to persist chat history after each service call during a tool-calling loop, enabling crash recovery and mid-run observability.|
## Running the samples from the console
@@ -3,7 +3,7 @@
// This sample shows how to create and use AI agents with Azure Foundry Agents as the backend.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -3,7 +3,7 @@
// This sample shows how to create and use a simple AI agent with Azure Foundry Agents as the backend.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -2,8 +2,9 @@
// This sample shows how to create and use a simple AI agent with a multi-turn conversation.
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -40,7 +40,7 @@ AgentResponse response = await agent.RunAsync("What is the weather like in Amste
// Check if there are any approval requests.
// For simplicity, we are assuming here that only function approvals are pending.
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
while (approvalRequests.Count > 0)
{
@@ -48,7 +48,7 @@ while (approvalRequests.Count > 0)
List<ChatMessage> userInputMessages = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {((FunctionCallContent)functionApprovalRequest.ToolCall).Name}");
bool approved = Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]);
});
@@ -56,7 +56,7 @@ while (approvalRequests.Count > 0)
// Pass the user input responses back to the agent for further processing.
response = await agent.RunAsync(userInputMessages, session);
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
@@ -24,7 +24,7 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: VisionName, model
ChatMessage message = new(ChatRole.User, [
new TextContent("What do you see in this image?"),
await DataContent.LoadFromAsync("assets/walkway.jpg"),
await DataContent.LoadFromAsync("Assets/walkway.jpg"),
]);
AgentSession session = await agent.CreateSessionAsync();
@@ -197,7 +197,7 @@ async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMes
AgentResponse response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
// For simplicity, we are assuming here that only function approvals are pending.
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
while (approvalRequests.Count > 0)
{
@@ -206,14 +206,14 @@ async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMes
response.Messages = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {((FunctionCallContent)functionApprovalRequest.ToolCall).Name}");
bool approved = Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]);
});
response = await innerAgent.RunAsync(response.Messages, session, options, cancellationToken);
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
}
return response;
@@ -4,7 +4,7 @@
using System.Text;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -3,7 +3,7 @@
// This sample shows how to use Computer Use Tool with AI Agents.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -3,7 +3,7 @@
// This sample shows how to use File Search Tool with AI Agents.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -3,7 +3,7 @@
// This sample shows how to use OpenAPI Tools with AI Agents.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
@@ -72,7 +72,7 @@ const string CountriesOpenApiSpec = """
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create the OpenAPI function definition
var openApiFunction = new OpenAPIFunctionDefinition(
var openApiFunction = new OpenApiFunctionDefinition(
"get_countries",
BinaryData.FromString(CountriesOpenApiSpec),
new OpenAPIAnonymousAuthenticationDetails())
@@ -3,7 +3,7 @@
// This sample shows how to use Bing Custom Search Tool with AI Agents.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
@@ -25,7 +25,7 @@ const string AgentInstructions = """
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Bing Custom Search tool parameters shared by both options
BingCustomSearchToolParameters bingCustomSearchToolParameters = new([
BingCustomSearchToolOptions bingCustomSearchToolParameters = new([
new BingCustomSearchConfiguration(connectionId, instanceName)
]);
@@ -3,7 +3,7 @@
// This sample shows how to use SharePoint Grounding Tool with AI Agents.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
@@ -3,7 +3,7 @@
// This sample shows how to use Microsoft Fabric Tool with AI Agents.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
@@ -3,7 +3,7 @@
// This sample shows how to use the Responses API Web Search Tool with AI Agents.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -13,7 +13,6 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.AI.Projects.OpenAI" />
</ItemGroup>
<ItemGroup>
@@ -4,8 +4,9 @@
// The Memory Search Tool enables agents to recall information from previous conversations,
// supporting user profile persistence and chat summaries across sessions.
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
@@ -36,7 +37,7 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
await EnsureMemoryStoreAsync();
// Create the Memory Search tool configuration
MemorySearchPreviewTool memorySearchTool = new(memoryStoreName, userScope) { UpdateDelay = 0 };
MemorySearchPreviewTool memorySearchTool = new(memoryStoreName, userScope) { UpdateDelayInSecs = 0 };
// Create agent using Option 1 (MEAI) or Option 2 (Native SDK)
AIAgent agent = await CreateAgentWithMEAI();
@@ -128,8 +129,8 @@ async Task EnsureMemoryStoreAsync()
MemoryUpdateResult updateResult = await aiProjectClient.MemoryStores.WaitForMemoriesUpdateAsync(
memoryStoreName: memoryStoreName,
options: memoryOptions,
pollingInterval: 500);
pollingInterval: 500,
options: memoryOptions);
if (updateResult.Status == MemoryStoreUpdateStatus.Failed)
{
@@ -9,12 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -4,7 +4,7 @@
// In this case the Azure Foundry Agents service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework.
// The sample first shows how to use MCP tools with auto approval, and then how to set up a tool that requires approval before it can be invoked and how to approve such a tool.
using Azure.AI.Agents.Persistent;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -16,7 +16,7 @@ var model = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new DefaultAzureCredential());
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// **** MCP Tool with Auto Approval ****
// *************************************
@@ -31,8 +31,8 @@ var mcpTool = new HostedMcpServerTool(
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire
};
// Create a server side persistent agent with the mcp tool, and expose it as an AIAgent.
AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
// Create a server side agent with the mcp tool, and expose it as an AIAgent.
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
model: model,
options: new()
{
@@ -49,7 +49,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.
await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
aiProjectClient.Agents.DeleteAgent(agent.Name);
// **** MCP Tool with Approval Required ****
// *****************************************
@@ -64,8 +64,8 @@ var mcpToolWithApproval = new HostedMcpServerTool(
ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire
};
// Create an agent based on Azure OpenAI Responses as the backend.
AIAgent agentWithRequiredApproval = await persistentAgentsClient.CreateAIAgentAsync(
// Create an agent with the MCP tool that requires approval.
AIAgent agentWithRequiredApproval = await aiProjectClient.CreateAIAgentAsync(
model: model,
options: new()
{
@@ -81,7 +81,7 @@ AIAgent agentWithRequiredApproval = await persistentAgentsClient.CreateAIAgentAs
// For simplicity, we are assuming here that only mcp tool approvals are pending.
AgentSession sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync();
AgentResponse response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval);
List<McpServerToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<McpServerToolApprovalRequestContent>().ToList();
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
while (approvalRequests.Count > 0)
{
@@ -89,11 +89,12 @@ while (approvalRequests.Count > 0)
List<ChatMessage> userInputResponses = approvalRequests
.ConvertAll(approvalRequest =>
{
McpServerToolCallContent mcpToolCall = (McpServerToolCallContent)approvalRequest.ToolCall!;
Console.WriteLine($"""
The agent would like to invoke the following MCP Tool, please reply Y to approve.
ServerName: {approvalRequest.ToolCall.ServerName}
Name: {approvalRequest.ToolCall.ToolName}
Arguments: {string.Join(", ", approvalRequest.ToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
ServerName: {mcpToolCall.ServerName}
Name: {mcpToolCall.Name}
Arguments: {string.Join(", ", mcpToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
""");
return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
});
@@ -101,7 +102,7 @@ while (approvalRequests.Count > 0)
// Pass the user input responses back to the agent for further processing.
response = await agentWithRequiredApproval.RunAsync(userInputResponses, sessionWithRequiredApproval);
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<McpServerToolApprovalRequestContent>().ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
@@ -33,8 +33,9 @@ var mcpTool = new HostedMcpServerTool(
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.GetResponsesClient()
.AsAIAgent(
model: deploymentName,
instructions: "You answer questions by searching the Microsoft Learn content only.",
name: "MicrosoftLearnAgent",
tools: [mcpTool]);
@@ -60,8 +61,9 @@ var mcpToolWithApproval = new HostedMcpServerTool(
AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.GetResponsesClient()
.AsAIAgent(
model: deploymentName,
instructions: "You answer questions by searching the Microsoft Learn content only.",
name: "MicrosoftLearnAgentWithApproval",
tools: [mcpToolWithApproval]);
@@ -70,7 +72,7 @@ AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
// For simplicity, we are assuming here that only mcp tool approvals are pending.
AgentSession sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync();
AgentResponse response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval);
List<McpServerToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<McpServerToolApprovalRequestContent>().ToList();
List<ToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
while (approvalRequests.Count > 0)
{
@@ -78,11 +80,12 @@ while (approvalRequests.Count > 0)
List<ChatMessage> userInputResponses = approvalRequests
.ConvertAll(approvalRequest =>
{
McpServerToolCallContent mcpToolCall = (McpServerToolCallContent)approvalRequest.ToolCall!;
Console.WriteLine($"""
The agent would like to invoke the following MCP Tool, please reply Y to approve.
ServerName: {approvalRequest.ToolCall.ServerName}
Name: {approvalRequest.ToolCall.ToolName}
Arguments: {string.Join(", ", approvalRequest.ToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
ServerName: {mcpToolCall.ServerName}
Name: {mcpToolCall.Name}
Arguments: {string.Join(", ", mcpToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
""");
return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
});
@@ -90,7 +93,7 @@ while (approvalRequests.Count > 0)
// Pass the user input responses back to the agent for further processing.
response = await agentWithRequiredApproval.RunAsync(userInputResponses, sessionWithRequiredApproval);
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<McpServerToolApprovalRequestContent>().ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
@@ -61,6 +61,12 @@ public static class Program
{
Console.WriteLine($"{outputEvent}");
}
if (evt is WorkflowErrorEvent errorEvent)
{
Console.WriteLine($"Workflow error: {errorEvent.Exception?.Message}");
Console.WriteLine($"Details: {errorEvent.Exception}");
}
}
}
}
@@ -175,7 +181,9 @@ internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEve
/// <summary>
/// A custom executor that uses an AI agent to provide feedback on a slogan.
/// </summary>
internal sealed class FeedbackExecutor : Executor<SloganResult>
[SendsMessage(typeof(FeedbackResult))]
[YieldsOutput(typeof(string))]
internal sealed partial class FeedbackExecutor : Executor<SloganResult>
{
private readonly AIAgent _agent;
private AgentSession? _session;
@@ -9,13 +9,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Agents.Persistent;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
@@ -20,60 +20,63 @@ public static class Program
{
private static async Task Main()
{
// Set up the Azure OpenAI client
// Set up the Azure AI Project client
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new AzureCliCredential());
// Create agents
AIAgent frenchAgent = await GetTranslationAgentAsync("French", persistentAgentsClient, deploymentName);
AIAgent spanishAgent = await GetTranslationAgentAsync("Spanish", persistentAgentsClient, deploymentName);
AIAgent englishAgent = await GetTranslationAgentAsync("English", persistentAgentsClient, deploymentName);
AIAgent frenchAgent = await CreateTranslationAgentAsync("French", aiProjectClient, deploymentName);
AIAgent spanishAgent = await CreateTranslationAgentAsync("Spanish", aiProjectClient, deploymentName);
AIAgent englishAgent = await CreateTranslationAgentAsync("English", aiProjectClient, deploymentName);
// Build the workflow by adding executors and connecting them
var workflow = new WorkflowBuilder(frenchAgent)
.AddEdge(frenchAgent, spanishAgent)
.AddEdge(spanishAgent, englishAgent)
.Build();
// Execute the workflow
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
// Must send the turn token to trigger the agents.
// The agents are wrapped as executors. When they receive messages,
// they will cache the messages and only start processing when they receive a TurnToken.
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
try
{
if (evt is AgentResponseUpdateEvent executorComplete)
// Build the workflow by adding executors and connecting them
var workflow = new WorkflowBuilder(frenchAgent)
.AddEdge(frenchAgent, spanishAgent)
.AddEdge(spanishAgent, englishAgent)
.Build();
// Execute the workflow
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
// Must send the turn token to trigger the agents.
// The agents are wrapped as executors. When they receive messages,
// they will cache the messages and only start processing when they receive a TurnToken.
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
if (evt is AgentResponseUpdateEvent executorComplete)
{
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
}
}
}
// Cleanup the agents created for the sample.
await persistentAgentsClient.Administration.DeleteAgentAsync(frenchAgent.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(spanishAgent.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(englishAgent.Id);
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);
}
}
/// <summary>
/// Creates a translation agent for the specified target language.
/// </summary>
/// <param name="targetLanguage">The target language for translation</param>
/// <param name="persistentAgentsClient">The PersistentAgentsClient to create the agent</param>
/// <param name="aiProjectClient">The <see cref="AIProjectClient"/> to create the agent with.</param>
/// <param name="model">The model to use for the agent</param>
/// <returns>A ChatClientAgent configured for the specified language</returns>
private static async Task<ChatClientAgent> GetTranslationAgentAsync(
private static async Task<ChatClientAgent> CreateTranslationAgentAsync(
string targetLanguage,
PersistentAgentsClient persistentAgentsClient,
AIProjectClient aiProjectClient,
string model)
{
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
model: model,
return await aiProjectClient.CreateAIAgentAsync(
name: $"{targetLanguage} Translator",
model: model,
instructions: $"You are a translation assistant that translates the provided text to {targetLanguage}.");
return await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
}
}
@@ -17,7 +17,7 @@
//
// Demonstrate:
// - Using custom GroupChatManager with agents that have approval-required tools.
// - Handling FunctionApprovalRequestContent in group chat scenarios.
// - Handling ToolApprovalRequestContent in group chat scenarios.
// - Multi-round group chat with tool approval interruption and resumption.
using System.ComponentModel;
@@ -101,16 +101,16 @@ public static class Program
{
case RequestInfoEvent e:
{
if (e.Request.TryGetDataAs(out FunctionApprovalRequestContent? approvalRequestContent))
if (e.Request.TryGetDataAs(out ToolApprovalRequestContent? approvalRequestContent))
{
Console.WriteLine();
Console.WriteLine($"[APPROVAL REQUIRED] From agent: {e.Request.PortInfo.PortId}");
Console.WriteLine($" Tool: {approvalRequestContent.FunctionCall.Name}");
Console.WriteLine($" Arguments: {JsonSerializer.Serialize(approvalRequestContent.FunctionCall.Arguments)}");
Console.WriteLine($" Tool: {((FunctionCallContent)approvalRequestContent.ToolCall).Name}");
Console.WriteLine($" Arguments: {JsonSerializer.Serialize(((FunctionCallContent)approvalRequestContent.ToolCall).Arguments)}");
Console.WriteLine();
// Approve the tool call request
Console.WriteLine($"Tool: {approvalRequestContent.FunctionCall.Name} approved");
Console.WriteLine($"Tool: {((FunctionCallContent)approvalRequestContent.ToolCall).Name} approved");
await run.SendResponseAsync(e.Request.CreateResponse(approvalRequestContent.CreateResponse(approved: true)));
}
@@ -9,7 +9,7 @@ using Microsoft.Extensions.AI;
namespace WorkflowAsAnAgentSample;
/// <summary>
/// This sample introduces the concepts workflows as agents, where a workflow can be
/// This sample introduces the concept of workflows as agents, where a workflow can be
/// treated as an <see cref="AIAgent"/>. This allows you to interact with a workflow
/// as if it were a single agent.
///
@@ -18,6 +18,14 @@ namespace WorkflowAsAnAgentSample;
///
/// You will interact with the workflow in an interactive loop, sending messages and receiving
/// streaming responses from the workflow as if it were an agent who responds in both languages.
///
/// This sample also demonstrates <see cref="IResettableExecutor"/>, which is required
/// for stateful executors that are shared across multiple workflow runs. Each iteration
/// of the interactive loop triggers a new workflow run against the same workflow instance.
/// Between runs, the framework automatically calls <see cref="IResettableExecutor.ResetAsync"/>
/// on shared executors so that accumulated state (e.g., collected messages) is cleared
/// before the next run begins. See <c>WorkflowFactory.ConcurrentAggregationExecutor</c>
/// for the implementation.
/// </summary>
/// <remarks>
/// Pre-requisites:
@@ -39,7 +47,10 @@ public static class Program
var agent = workflow.AsAIAgent("workflow-agent", "Workflow Agent");
var session = await agent.CreateSessionAsync();
// Start an interactive loop to interact with the workflow as if it were an agent
// Start an interactive loop to interact with the workflow as if it were an agent.
// Each iteration runs the workflow again on the same workflow instance. Between runs,
// the framework calls IResettableExecutor.ResetAsync() on shared stateful executors
// (like ConcurrentAggregationExecutor) to clear accumulated state from the previous run.
while (true)
{
Console.WriteLine();
@@ -10,6 +10,14 @@ internal static class WorkflowFactory
{
/// <summary>
/// Creates a workflow that uses two language agents to process input concurrently.
///
/// In this workflow, the <c>Start</c> <see cref="ChatForwardingExecutor"/> and the
/// <see cref="ConcurrentAggregationExecutor"/> are provided as shared instances, meaning
/// the same executor objects are reused across multiple workflow runs. The language agents
/// (French and English) are created via a factory and instantiated per workflow run.
/// Stateful shared executors must implement <see cref="IResettableExecutor"/> so the
/// framework can clear their state between runs. Framework-provided executors like
/// <see cref="ChatForwardingExecutor"/> already implement this interface.
/// </summary>
/// <param name="chatClient">The chat client to use for the agents</param>
/// <returns>A workflow that processes input using two language agents</returns>
@@ -40,7 +48,18 @@ internal static class WorkflowFactory
/// <summary>
/// Executor that aggregates the results from the concurrent agents.
///
/// This executor is stateful — it accumulates messages in <see cref="_messages"/>
/// as they arrive from each agent. Because it is provided as a shared instance
/// (not via a factory), the same object is reused across workflow runs. Implementing
/// <see cref="IResettableExecutor"/> allows the framework to call <see cref="ResetAsync"/>
/// between runs, clearing accumulated state so each run starts fresh.
///
/// Without <see cref="IResettableExecutor"/>, attempting to reuse a workflow containing
/// shared executor instances that do not implement this interface would throw an
/// <see cref="InvalidOperationException"/>.
/// </summary>
[YieldsOutput(typeof(string))]
private sealed class ConcurrentAggregationExecutor() :
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor"), IResettableExecutor
{
@@ -64,7 +83,11 @@ internal static class WorkflowFactory
}
}
/// <inheritdoc/>
/// <summary>
/// Resets the executor state between workflow runs by clearing accumulated messages.
/// The framework calls this automatically when a workflow run completes, before the
/// workflow can be used for another run.
/// </summary>
public ValueTask ResetAsync()
{
this._messages.Clear();
@@ -41,6 +41,7 @@ internal enum NumberSignal
/// <summary>
/// Executor that makes a guess based on the current bounds.
/// </summary>
[SendsMessage(typeof(int))]
internal sealed class GuessNumberExecutor() : Executor<NumberSignal>("Guess")
{
/// <summary>
@@ -104,6 +105,8 @@ internal sealed class GuessNumberExecutor() : Executor<NumberSignal>("Guess")
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
[SendsMessage(typeof(NumberSignal))]
[YieldsOutput(typeof(string))]
internal sealed class JudgeExecutor() : Executor<int>("Judge")
{
private readonly int _targetNumber;
@@ -41,6 +41,7 @@ internal enum NumberSignal
/// <summary>
/// Executor that makes a guess based on the current bounds.
/// </summary>
[SendsMessage(typeof(int))]
internal sealed class GuessNumberExecutor() : Executor<NumberSignal>("Guess")
{
/// <summary>
@@ -104,6 +105,8 @@ internal sealed class GuessNumberExecutor() : Executor<NumberSignal>("Guess")
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
[SendsMessage(typeof(NumberSignal))]
[YieldsOutput(typeof(string))]
internal sealed class JudgeExecutor() : Executor<int>("Judge")
{
private readonly int _targetNumber;
@@ -53,6 +53,8 @@ internal sealed class SignalWithNumber
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
[SendsMessage(typeof(SignalWithNumber))]
[YieldsOutput(typeof(string))]
internal sealed class JudgeExecutor() : Executor<int>("Judge")
{
private readonly int _targetNumber;
@@ -72,6 +72,8 @@ public static class Program
/// <summary>
/// Executor that starts the concurrent processing by sending messages to the agents.
/// </summary>
[SendsMessage(typeof(ChatMessage))]
[SendsMessage(typeof(TurnToken))]
internal sealed partial class ConcurrentStartExecutor() :
Executor("ConcurrentStartExecutor")
{
@@ -97,7 +99,8 @@ internal sealed partial class ConcurrentStartExecutor() :
/// <summary>
/// Executor that aggregates the results from the concurrent agents.
/// </summary>
internal sealed class ConcurrentAggregationExecutor() :
[YieldsOutput(typeof(string))]
internal sealed partial class ConcurrentAggregationExecutor() :
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor")
{
private readonly List<ChatMessage> _messages = [];
@@ -128,6 +128,7 @@ public static class Program
/// <summary>
/// Splits data into roughly equal chunks based on the number of mapper nodes.
/// </summary>
[SendsMessage(typeof(SplitComplete))]
internal sealed class Split(string[] mapperIds, string id) :
Executor<string>(id)
{
@@ -186,6 +187,7 @@ internal sealed class Split(string[] mapperIds, string id) :
/// <summary>
/// Maps each token to a count of 1 and writes pairs to a per-mapper file.
/// </summary>
[SendsMessage(typeof(MapComplete))]
internal sealed class Mapper(string id) : Executor<SplitComplete>(id)
{
/// <summary>
@@ -212,6 +214,7 @@ internal sealed class Mapper(string id) : Executor<SplitComplete>(id)
/// <summary>
/// Groups intermediate pairs by key and partitions them across reducers.
/// </summary>
[SendsMessage(typeof(ShuffleComplete))]
internal sealed class Shuffler(string[] reducerIds, string[] mapperIds, string id) :
Executor<MapComplete>(id)
{
@@ -311,6 +314,7 @@ internal sealed class Shuffler(string[] reducerIds, string[] mapperIds, string i
/// <summary>
/// Sums grouped counts per key for its assigned partition.
/// </summary>
[SendsMessage(typeof(ReduceComplete))]
internal sealed class Reducer(string id) : Executor<ShuffleComplete>(id)
{
/// <summary>
@@ -352,6 +356,7 @@ internal sealed class Reducer(string id) : Executor<ShuffleComplete>(id)
/// <summary>
/// Joins all reducer outputs and yields the final output.
/// </summary>
[YieldsOutput(typeof(List<string>))]
internal sealed class CompletionExecutor(string id) :
Executor<List<ReduceComplete>>(id)
{
@@ -228,6 +228,7 @@ internal sealed class EmailAssistantExecutor : Executor<DetectionResult, EmailRe
/// <summary>
/// Executor that sends emails.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class SendEmailExecutor() : Executor<EmailResponse>("SendEmailExecutor")
{
/// <summary>
@@ -240,6 +241,7 @@ internal sealed class SendEmailExecutor() : Executor<EmailResponse>("SendEmailEx
/// <summary>
/// Executor that handles spam messages.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class HandleSpamExecutor() : Executor<DetectionResult>("HandleSpamExecutor")
{
/// <summary>
@@ -252,6 +252,7 @@ internal sealed class EmailAssistantExecutor : Executor<DetectionResult, EmailRe
/// <summary>
/// Executor that sends emails.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class SendEmailExecutor() : Executor<EmailResponse>("SendEmailExecutor")
{
/// <summary>
@@ -264,6 +265,7 @@ internal sealed class SendEmailExecutor() : Executor<EmailResponse>("SendEmailEx
/// <summary>
/// Executor that handles spam messages.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class HandleSpamExecutor() : Executor<DetectionResult>("HandleSpamExecutor")
{
/// <summary>
@@ -285,6 +287,7 @@ internal sealed class HandleSpamExecutor() : Executor<DetectionResult>("HandleSp
/// <summary>
/// Executor that handles uncertain emails.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class HandleUncertainExecutor() : Executor<DetectionResult>("HandleUncertainExecutor")
{
/// <summary>
@@ -310,6 +310,7 @@ internal sealed class EmailAssistantExecutor : Executor<AnalysisResult, EmailRes
/// <summary>
/// Executor that sends emails.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class SendEmailExecutor() : Executor<EmailResponse>("SendEmailExecutor")
{
/// <summary>
@@ -322,6 +323,7 @@ internal sealed class SendEmailExecutor() : Executor<EmailResponse>("SendEmailEx
/// <summary>
/// Executor that handles spam messages.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class HandleSpamExecutor() : Executor<AnalysisResult>("HandleSpamExecutor")
{
/// <summary>
@@ -343,6 +345,7 @@ internal sealed class HandleSpamExecutor() : Executor<AnalysisResult>("HandleSpa
/// <summary>
/// Executor that handles uncertain messages.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class HandleUncertainExecutor() : Executor<AnalysisResult>("HandleUncertainExecutor")
{
/// <summary>
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using OpenAI.Responses;
@@ -275,7 +275,7 @@ internal sealed class Program
Tools =
{
AgentTool.CreateOpenApiTool(
new OpenAPIFunctionDefinition(
new OpenApiFunctionDefinition(
"weather-forecast",
BinaryData.FromString(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "wttr.json"))),
new OpenAPIAnonymousAuthenticationDetails()))
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
@@ -3,8 +3,9 @@
// Uncomment this to enable JSON checkpointing to the local file system.
//#define CHECKPOINT_JSON
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using OpenAI.Responses;
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
@@ -5,7 +5,7 @@
// invoked to perform specific tasks, like searching documentation or executing operations.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI.Workflows.Declarative.Mcp;
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Shared.Foundry;
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Shared.Foundry;
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using OpenAI.Responses;
@@ -38,6 +38,8 @@ internal enum NumberSignal
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
[SendsMessage(typeof(NumberSignal))]
[YieldsOutput(typeof(string))]
internal sealed class JudgeExecutor() : Executor<int>("Judge")
{
private readonly int _targetNumber;
+4 -2
View File
@@ -56,6 +56,7 @@ internal enum NumberSignal
/// <summary>
/// Executor that makes a guess based on the current bounds.
/// </summary>
[SendsMessage(typeof(int))]
internal sealed class GuessNumberExecutor : Executor<NumberSignal>
{
/// <summary>
@@ -104,6 +105,8 @@ internal sealed class GuessNumberExecutor : Executor<NumberSignal>
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
[SendsMessage(typeof(NumberSignal))]
[YieldsOutput(typeof(string))]
internal sealed class JudgeExecutor : Executor<int>
{
private readonly int _targetNumber;
@@ -124,8 +127,7 @@ internal sealed class JudgeExecutor : Executor<int>
this._tries++;
if (message == this._targetNumber)
{
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken)
;
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken);
}
else if (message < this._targetNumber)
{
@@ -99,6 +99,10 @@ internal sealed class ParagraphCountingExecutor() : Executor<string, FileStats>(
}
}
/// <summary>
/// The aggregation executor collects results from both executors and yields the final output.
/// </summary>
[YieldsOutput(typeof(string))]
internal sealed class AggregationExecutor() : Executor<FileStats>("AggregationExecutor")
{
private readonly List<FileStats> _messages = [];
@@ -205,6 +205,8 @@ internal sealed class TextInverterExecutor(string id) : Executor<string, string>
/// 1. Sending ChatMessage(s)
/// 2. Sending a TurnToken to trigger processing
/// </summary>
[SendsMessage(typeof(ChatMessage))]
[SendsMessage(typeof(TurnToken))]
internal sealed class StringToChatMessageExecutor(string id) : Executor<string>(id)
{
public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
@@ -234,6 +236,8 @@ internal sealed class StringToChatMessageExecutor(string id) : Executor<string>(
/// The AIAgentHostExecutor sends response.Messages which has runtime type List&lt;ChatMessage&gt;.
/// The message router uses exact type matching via message.GetType().
/// </remarks>
[SendsMessage(typeof(ChatMessage))]
[SendsMessage(typeof(TurnToken))]
internal sealed class JailbreakSyncExecutor() : Executor<List<ChatMessage>>("JailbreakSync")
{
public override async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context, CancellationToken cancellationToken = default)
@@ -14,7 +14,6 @@
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>WorkflowMcpTool</AssemblyName>
<RootNamespace>WorkflowMcpTool</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace WorkflowMcpTool;
internal sealed class TranslateText() : Executor<string, TranslationResult>("TranslateText")
{
public override ValueTask<TranslationResult> HandleAsync(
string message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Activity] TranslateText: '{message}'");
return ValueTask.FromResult(new TranslationResult(message, message.ToUpperInvariant()));
}
}
internal sealed class FormatOutput() : Executor<TranslationResult, string>("FormatOutput")
{
public override ValueTask<string> HandleAsync(
TranslationResult message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine("[Activity] FormatOutput: Formatting result");
return ValueTask.FromResult($"Original: {message.Original} => Translated: {message.Translated}");
}
}
internal sealed class LookupOrder() : Executor<string, OrderInfo>("LookupOrder")
{
public override ValueTask<OrderInfo> HandleAsync(
string message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Activity] LookupOrder: '{message}'");
return ValueTask.FromResult(new OrderInfo(message, "Alice Johnson", "Wireless Headphones", Quantity: 2, UnitPrice: 49.99m));
}
}
internal sealed class EnrichOrder() : Executor<OrderInfo, OrderSummary>("EnrichOrder")
{
public override ValueTask<OrderSummary> HandleAsync(
OrderInfo message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Activity] EnrichOrder: '{message.OrderId}'");
return ValueTask.FromResult(new OrderSummary(message, TotalPrice: message.Quantity * message.UnitPrice, Status: "Confirmed"));
}
}
internal sealed record TranslationResult(string Original, string Translated);
internal sealed record OrderInfo(string OrderId, string CustomerName, string Product, int Quantity, decimal UnitPrice);
internal sealed record OrderSummary(OrderInfo Order, decimal TotalPrice, string Status);
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to expose a durable workflow as an MCP (Model Context Protocol) tool.
// When using AddWorkflow with exposeMcpToolTrigger: true, the Functions host will automatically
// generate a remote MCP endpoint for the app at /runtime/webhooks/mcp with a workflow-specific
// tool name. MCP-compatible clients can then invoke the workflow as a tool.
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using WorkflowMcpTool;
// Define executors
TranslateText translateText = new();
FormatOutput formatOutput = new();
LookupOrder lookupOrder = new();
EnrichOrder enrichOrder = new();
// Build a simple workflow: TranslateText -> FormatOutput
Workflow translateWorkflow = new WorkflowBuilder(translateText)
.WithName("Translate")
.WithDescription("Translate text to uppercase and format the result")
.AddEdge(translateText, formatOutput)
.Build();
// Build a workflow that returns a POCO: LookupOrder -> EnrichOrder
Workflow orderLookupWorkflow = new WorkflowBuilder(lookupOrder)
.WithName("OrderLookup")
.WithDescription("Look up an order by ID and return enriched order details")
.AddEdge(lookupOrder, enrichOrder)
.Build();
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableWorkflows(workflows =>
{
// Expose both workflows as MCP tool triggers.
workflows.AddWorkflow(translateWorkflow, exposeStatusEndpoint: false, exposeMcpToolTrigger: true);
workflows.AddWorkflow(orderLookupWorkflow, exposeStatusEndpoint: false, exposeMcpToolTrigger: true);
})
.Build();
app.Run();
@@ -0,0 +1,81 @@
# Workflow as MCP Tool Sample
This sample demonstrates how to expose durable workflows as [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) tools, enabling MCP-compatible clients to invoke workflows directly.
## Key Concepts Demonstrated
- **Workflow as MCP Tool**: Expose workflows as callable MCP tools using `exposeMcpToolTrigger: true`
- **MCP Server Hosting**: The Azure Functions host automatically generates a remote MCP endpoint at `/runtime/webhooks/mcp`
- **String and POCO Results**: Shows workflows returning both plain strings and structured JSON objects
## Sample Architecture
The sample creates two workflows exposed as MCP tools:
### Translate Workflow (returns a string)
| Executor | Input | Output | Description |
|----------|-------|--------|-------------|
| **TranslateText** | `string` | `TranslationResult` | Converts input text to uppercase |
| **FormatOutput** | `TranslationResult` | `string` | Formats the result into a readable string |
### OrderLookup Workflow (returns a POCO)
| Executor | Input | Output | Description |
|----------|-------|--------|-------------|
| **LookupOrder** | `string` | `OrderInfo` | Looks up an order by ID |
| **EnrichOrder** | `OrderInfo` | `OrderSummary` | Adds computed fields (total price, status) |
## Environment Setup
See the [README.md](../../README.md) file in the parent directory for complete setup instructions, including:
- Prerequisites installation
- Durable Task Scheduler setup
- Storage emulator configuration
For this sample, you'll also need [Node.js](https://nodejs.org/en/download) to use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).
## Running the Sample
1. **Start the Function App**:
```bash
cd dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool
func start
```
2. **Note the MCP Server Endpoint**: When the app starts, you'll see the MCP server endpoint in the terminal output:
```text
MCP server endpoint: http://localhost:7071/runtime/webhooks/mcp
```
## Invoking Workflows via MCP Inspector
1. Install and run the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
```bash
npx @modelcontextprotocol/inspector
```
2. Connect to the MCP server endpoint:
- For **Transport Type**, select **"Streamable HTTP"**
- For **URL**, enter `http://localhost:7071/runtime/webhooks/mcp`
- Click the **Connect** button
3. Click the **List Tools** button. You should see two tools: `Translate` and `OrderLookup`.
4. Test the **Translate** tool (returns a plain string):
- Select the `Translate` tool
- Set `hello world` as the `input` parameter
- Click **Run Tool**
- Expected result: `Original: hello world => Translated: HELLO WORLD`
5. Test the **OrderLookup** tool (returns a JSON object):
- Select the `OrderLookup` tool
- Set `ORD-2025-42` as the `input` parameter
- Click **Run Tool**
- Expected result: A JSON object containing order details such as `OrderId`, `CustomerName`, `Product`, `TotalPrice`, and `Status`
You'll see the workflow executor activities logged in the terminal where you ran `func start`.
@@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,8 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>WorkflowAndAgents</AssemblyName>
<RootNamespace>WorkflowAndAgents</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace WorkflowAndAgents;
internal sealed class TranslateText() : Executor<string, TranslationResult>("TranslateText")
{
public override ValueTask<TranslationResult> HandleAsync(
string message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Activity] TranslateText: '{message}'");
return ValueTask.FromResult(new TranslationResult(message, message.ToUpperInvariant()));
}
}
internal sealed class FormatOutput() : Executor<TranslationResult, string>("FormatOutput")
{
public override ValueTask<string> HandleAsync(
TranslationResult message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine("[Activity] FormatOutput: Formatting result");
return ValueTask.FromResult($"Original: {message.Original} => Translated: {message.Translated}");
}
}
internal sealed record TranslationResult(string Original, string Translated);

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