Merge branch 'main' into dev/dotnet_workflow/fix_file_checkpointstore_paths

This commit is contained in:
Jacob Alber
2026-03-23 13:20:50 -04:00
committed by GitHub
227 changed files with 5839 additions and 924 deletions
+17 -18
View File
@@ -19,11 +19,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" />
@@ -40,12 +39,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 -->
@@ -64,12 +63,12 @@
<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.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
@@ -77,11 +76,11 @@
<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.3" />
<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.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<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" />
@@ -111,9 +110,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" />
@@ -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
{
@@ -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.
@@ -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."));
@@ -23,7 +23,7 @@ var skillsProvider = new FileAgentSkillsProvider(skillPath: Path.Combine(AppCont
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "SkillsAgent",
@@ -32,7 +32,8 @@ AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredent
Instructions = "You are a helpful assistant.",
},
AIContextProviders = [skillsProvider],
});
},
model: deploymentName);
// --- Example 1: Expense policy question (loads FAQ resource) ---
Console.WriteLine("Example 1: Checking expense policy FAQ");
@@ -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("{}")));
@@ -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());
@@ -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;
@@ -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}");
@@ -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}");
@@ -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)));
}
@@ -41,6 +41,7 @@ internal static class WorkflowFactory
/// <summary>
/// Executor that aggregates the results from the concurrent agents.
/// </summary>
[YieldsOutput(typeof(string))]
private sealed class ConcurrentAggregationExecutor() :
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor"), IResettableExecutor
{
@@ -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)
@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
@@ -23,7 +23,7 @@
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hosting.A2A\Microsoft.Agents.AI.Hosting.A2A.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.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.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using A2A;
using Azure.AI.Agents.Persistent;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -12,16 +12,15 @@ namespace A2AServer;
internal static class HostAgentFactory
{
internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string assistantId, IList<AITool>? tools = null)
internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string agentName, IList<AITool>? tools = null)
{
// 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());
PersistentAgent persistentAgent = await persistentAgentsClient.Administration.GetAgentAsync(assistantId);
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
AIAgent agent = await persistentAgentsClient
.GetAIAgentAsync(persistentAgent.Id, chatOptions: new() { Tools = tools });
AIAgent agent = await aiProjectClient
.GetAIAgentAsync(agentName, tools: tools);
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
@@ -8,16 +8,16 @@ using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
string agentId = string.Empty;
string agentName = string.Empty;
string agentType = string.Empty;
for (var i = 0; i < args.Length; i++)
{
if (args[i].StartsWith("--agentId", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length)
if (args[i].Equals("--agentName", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
agentId = args[++i];
agentName = args[++i];
}
else if (args[i].StartsWith("--agentType", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length)
else if (args[i].Equals("--agentType", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
agentType = args[++i];
}
@@ -50,13 +50,13 @@ IList<AITool> tools =
AIAgent hostA2AAgent;
AgentCard hostA2AAgentCard;
if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentId))
if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentName))
{
(hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
{
"INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentId, tools),
"POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentId),
"LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentId),
"INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, tools),
"POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName),
"LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
@@ -101,7 +101,7 @@ else if (!string.IsNullOrEmpty(apiKey))
}
else
{
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentId must be provided");
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentName must be provided");
}
var a2aTaskManager = app.MapA2A(
@@ -90,15 +90,15 @@ $env:AZURE_AI_PROJECT_ENDPOINT="https://ai-foundry-your-project.services.ai.azur
Use the following commands to run each A2A server
```bash
dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentId "<Invoice Agent Id>" --agentType "invoice" --no-build
dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentName "<Invoice Agent Name>" --agentType "invoice" --no-build
```
```bash
dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentId "<Policy Agent Id>" --agentType "policy" --no-build
dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentName "<Policy Agent Name>" --agentType "policy" --no-build
```
```bash
dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentId "<Logistics Agent Id>" --agentType "logistics" --no-build
dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentName "<Logistics Agent Name>" --agentType "logistics" --no-build
```
### Testing the Agents using the Rest Client
@@ -27,7 +27,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
Transport = new HttpClientPipelineTransport(httpClient)
};
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
var openAiClient = new ResponsesClient(credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient(agentName);
var chatOptions = new ChatOptions()
{
ConversationId = sessionId
@@ -30,8 +30,8 @@ TokenCredential browserCredential = new InteractiveBrowserCredential(
using IChatClient client = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.AsIChatClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsBuilder()
.WithPurview(browserCredential, new PurviewSettings("Agent Framework Test App"))
.Build();
@@ -37,7 +37,7 @@
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
@@ -36,9 +36,10 @@
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.4.0" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -31,7 +31,8 @@ AITool mcpTool = new HostedMcpServerTool(serverName: "microsoft_learn", serverAd
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetResponsesClient(deploymentName)
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsAIAgent(
instructions: "You answer questions by searching the Microsoft Learn content only.",
name: "MicrosoftLearnAgent",
@@ -38,7 +38,7 @@
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
@@ -36,7 +36,7 @@
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
@@ -36,7 +36,7 @@
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
@@ -36,7 +36,7 @@
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc1" />
</ItemGroup>
@@ -122,7 +122,7 @@ internal sealed class AFAgentApplication : AgentApplication
&& valueElement.GetProperty("requestJson") is JsonElement requestJsonElement
&& requestJsonElement.ValueKind == JsonValueKind.String)
{
var requestContent = JsonSerializer.Deserialize<FunctionApprovalRequestContent>(requestJsonElement.GetString()!, JsonUtilities.DefaultOptions);
var requestContent = JsonSerializer.Deserialize<ToolApprovalRequestContent>(requestJsonElement.GetString()!, JsonUtilities.DefaultOptions);
return new ChatMessage(ChatRole.User, [requestContent!.CreateResponse(approvedJsonElement.ValueKind == JsonValueKind.True)]);
}
@@ -138,7 +138,7 @@ internal sealed class AFAgentApplication : AgentApplication
/// <param name="attachments">The list of <see cref="Attachment"/> to which the adaptive cards will be added.</param>
private static void HandleUserInputRequests(AgentResponse response, ref List<Attachment>? attachments)
{
foreach (FunctionApprovalRequestContent functionApprovalRequest in response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>())
foreach (ToolApprovalRequestContent functionApprovalRequest in response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>())
{
var functionApprovalRequestJson = JsonSerializer.Serialize(functionApprovalRequest, JsonUtilities.DefaultOptions);
@@ -152,7 +152,7 @@ internal sealed class AFAgentApplication : AgentApplication
});
card.Body.Add(new AdaptiveTextBlock
{
Text = $"Function: {functionApprovalRequest.FunctionCall.Name}"
Text = $"Function: {((FunctionCallContent)functionApprovalRequest.ToolCall).Name}"
});
card.Body.Add(new AdaptiveActionSet()
{
@@ -20,6 +20,8 @@
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework AzureAI Persistent Agents</Title>
<Description>Provides Microsoft Agent Framework support for Azure AI Persistent Agents.</Description>
<!-- Disabled until Azure.AI.Agents.Persistent targets ME.AI 10.4.0+ (https://github.com/microsoft/agent-framework/issues/4769) -->
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>
@@ -19,6 +19,7 @@ public static class PersistentAgentsClientExtensions
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
[Obsolete("Please use the latest Foundry Agents service via the Microsoft.Agents.AI.AzureAI package.")]
public static ChatClientAgent AsAIAgent(
this PersistentAgentsClient persistentAgentsClient,
Response<PersistentAgent> persistentAgentResponse,
@@ -43,6 +44,7 @@ public static class PersistentAgentsClientExtensions
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
[Obsolete("Please use the latest Foundry Agents service via the Microsoft.Agents.AI.AzureAI package.")]
public static ChatClientAgent AsAIAgent(
this PersistentAgentsClient persistentAgentsClient,
PersistentAgent persistentAgentMetadata,
@@ -93,6 +95,7 @@ public static class PersistentAgentsClientExtensions
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
[Obsolete("Please use the latest Foundry Agents service via the Microsoft.Agents.AI.AzureAI package.")]
public static async Task<ChatClientAgent> GetAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string agentId,
@@ -125,6 +128,7 @@ public static class PersistentAgentsClientExtensions
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentResponse"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
[Obsolete("Please use the latest Foundry Agents service via the Microsoft.Agents.AI.AzureAI package.")]
public static ChatClientAgent AsAIAgent(
this PersistentAgentsClient persistentAgentsClient,
Response<PersistentAgent> persistentAgentResponse,
@@ -150,6 +154,7 @@ public static class PersistentAgentsClientExtensions
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentMetadata"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
[Obsolete("Please use the latest Foundry Agents service via the Microsoft.Agents.AI.AzureAI package.")]
public static ChatClientAgent AsAIAgent(
this PersistentAgentsClient persistentAgentsClient,
PersistentAgent persistentAgentMetadata,
@@ -211,6 +216,7 @@ public static class PersistentAgentsClientExtensions
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentsClient"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="agentId"/> is empty or whitespace.</exception>
[Obsolete("Please use the latest Foundry Agents service via the Microsoft.Agents.AI.AzureAI package.")]
public static async Task<ChatClientAgent> GetAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string agentId,
@@ -256,6 +262,7 @@ public static class PersistentAgentsClientExtensions
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
[Obsolete("Please use the latest Foundry Agents service via the Microsoft.Agents.AI.AzureAI package.")]
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string model,
@@ -306,6 +313,7 @@ public static class PersistentAgentsClientExtensions
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentsClient"/> or <paramref name="model"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
[Obsolete("Please use the latest Foundry Agents service via the Microsoft.Agents.AI.AzureAI package.")]
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string model,
@@ -0,0 +1,19 @@
# Microsoft.Agents.AI.AzureAI.Persistent
Provides integration between the Microsoft Agent Framework and Azure AI Agents Persistent (`Azure.AI.Agents.Persistent`).
## ⚠️ Known Compatibility Limitation
The underlying `Azure.AI.Agents.Persistent` package (currently 1.2.0-beta.9) targets `Microsoft.Extensions.AI.Abstractions` 10.1.x and references types that were renamed in 10.4.0 (e.g., `McpServerToolApprovalResponseContent``ToolApprovalResponseContent`). This causes `TypeLoadException` at runtime when used with ME.AI 10.4.0+.
**Compatible versions:**
| Package | Compatible Version |
|---|---|
| `Azure.AI.Agents.Persistent` | 1.2.0-beta.9 (targets ME.AI 10.1.x) |
| `Microsoft.Extensions.AI.Abstractions` | ≤ 10.3.0 |
| `OpenAI` | ≤ 2.8.0 |
**Resolution:** An updated version of `Azure.AI.Agents.Persistent` targeting ME.AI 10.4.0+ is expected in 1.2.0-beta.10. The upstream fix is tracked in [Azure/azure-sdk-for-net#56929](https://github.com/Azure/azure-sdk-for-net/pull/56929).
**Tracking issue:** [microsoft/agent-framework#4769](https://github.com/microsoft/agent-framework/issues/4769)
@@ -2,8 +2,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
@@ -57,7 +58,7 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
/// The <see cref="IChatClient"/> provided should be decorated with a <see cref="AzureAIProjectChatClient"/> for proper functionality.
/// </remarks>
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentRecord agentRecord, ChatOptions? chatOptions)
: this(aiProjectClient, Throw.IfNull(agentRecord).Versions.Latest, chatOptions)
: this(aiProjectClient, Throw.IfNull(agentRecord).GetLatestVersion(), chatOptions)
{
this._agentRecord = agentRecord;
}
@@ -9,7 +9,8 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects.Agents;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AzureAI;
using Microsoft.Extensions.AI;
@@ -189,7 +190,7 @@ public static partial class AzureAIProjectChatClientExtensions
ThrowIfInvalidAgentName(options.Name);
AgentRecord agentRecord = await GetAgentRecordByNameAsync(aiProjectClient, options.Name, cancellationToken).ConfigureAwait(false);
var agentVersion = agentRecord.Versions.Latest;
var agentVersion = agentRecord.GetLatestVersion();
var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: !options.UseProvidedChatClientAsIs);
@@ -361,7 +362,7 @@ public static partial class AzureAIProjectChatClientExtensions
{
ClientResult protocolResponse = await aiProjectClient.Agents.GetAgentAsync(agentName, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
var rawResponse = protocolResponse.GetRawResponse();
AgentRecord? result = ModelReaderWriter.Read<AgentRecord>(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsOpenAIContext.Default);
AgentRecord? result = ModelReaderWriter.Read<AgentRecord>(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default);
return result ?? throw new InvalidOperationException($"Agent with name '{agentName}' not found.");
}
@@ -370,11 +371,11 @@ public static partial class AzureAIProjectChatClientExtensions
/// </summary>
private static async Task<AgentVersion> CreateAgentVersionWithProtocolAsync(AIProjectClient aiProjectClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken)
{
BinaryData serializedOptions = ModelReaderWriter.Write(creationOptions, s_modelWriterOptionsWire, AzureAIProjectsContext.Default);
BinaryData serializedOptions = ModelReaderWriter.Write(creationOptions, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default);
BinaryContent content = BinaryContent.Create(serializedOptions);
ClientResult protocolResponse = await aiProjectClient.Agents.CreateAgentVersionAsync(agentName, content, foundryFeatures: null, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
var rawResponse = protocolResponse.GetRawResponse();
AgentVersion? result = ModelReaderWriter.Read<AgentVersion>(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsOpenAIContext.Default);
AgentVersion? result = ModelReaderWriter.Read<AgentVersion>(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default);
return result ?? throw new InvalidOperationException($"Failed to create agent version for agent '{agentName}'.");
}
@@ -485,7 +486,7 @@ public static partial class AzureAIProjectChatClientExtensions
=> AsChatClientAgent(
AIProjectClient,
agentRecord,
CreateChatClientAgentOptions(agentRecord.Versions.Latest, new ChatOptions() { Tools = tools }, requireInvocableTools),
CreateChatClientAgentOptions(agentRecord.GetLatestVersion(), new ChatOptions() { Tools = tools }, requireInvocableTools),
clientFactory,
services);
@@ -15,7 +15,6 @@
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.AI.Projects.OpenAI" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="OpenAI" />
@@ -196,8 +196,8 @@ internal static class AgentResponseUpdateExtensions
TextReasoningContent => new TextReasoningContentEventGenerator(context.IdGenerator, seq, outputIndex),
FunctionCallContent => new FunctionCallEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
FunctionResultContent => new FunctionResultEventGenerator(context.IdGenerator, seq, outputIndex),
FunctionApprovalRequestContent => new FunctionApprovalRequestEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
FunctionApprovalResponseContent => new FunctionApprovalResponseEventGenerator(context.IdGenerator, seq, outputIndex),
ToolApprovalRequestContent => new ToolApprovalRequestEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
ToolApprovalResponseContent => new ToolApprovalResponseEventGenerator(context.IdGenerator, seq, outputIndex),
ErrorContent => new ErrorContentEventGenerator(context.IdGenerator, seq, outputIndex),
UriContent uriContent when uriContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
@@ -12,33 +12,37 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
/// A generator for streaming events from function approval request content.
/// This is a non-standard DevUI extension for human-in-the-loop scenarios.
/// </summary>
internal sealed class FunctionApprovalRequestEventGenerator(
internal sealed class ToolApprovalRequestEventGenerator(
IdGenerator idGenerator,
SequenceNumber seq,
int outputIndex,
JsonSerializerOptions jsonSerializerOptions) : StreamingEventGenerator
{
public override bool IsSupported(AIContent content) => content is FunctionApprovalRequestContent;
public override bool IsSupported(AIContent content) => content is ToolApprovalRequestContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (content is not FunctionApprovalRequestContent approvalRequest)
if (content is not ToolApprovalRequestContent approvalRequest)
{
throw new InvalidOperationException("FunctionApprovalRequestEventGenerator only supports FunctionApprovalRequestContent.");
throw new InvalidOperationException("ToolApprovalRequestEventGenerator only supports ToolApprovalRequestContent.");
}
if (approvalRequest.ToolCall is not FunctionCallContent functionCall)
{
yield break;
}
yield return new StreamingFunctionApprovalRequested
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
RequestId = approvalRequest.Id,
RequestId = approvalRequest.RequestId,
ItemId = idGenerator.GenerateMessageId(),
FunctionCall = new FunctionCallInfo
{
Id = approvalRequest.FunctionCall.CallId,
Name = approvalRequest.FunctionCall.Name,
Id = functionCall.CallId,
Name = functionCall.Name,
Arguments = JsonSerializer.SerializeToElement(
approvalRequest.FunctionCall.Arguments,
functionCall.Arguments,
jsonSerializerOptions.GetTypeInfo(typeof(IDictionary<string, object>)))
}
};
@@ -11,25 +11,25 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
/// A generator for streaming events from function approval response content.
/// This is a non-standard DevUI extension for human-in-the-loop scenarios.
/// </summary>
internal sealed class FunctionApprovalResponseEventGenerator(
internal sealed class ToolApprovalResponseEventGenerator(
IdGenerator idGenerator,
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
public override bool IsSupported(AIContent content) => content is FunctionApprovalResponseContent;
public override bool IsSupported(AIContent content) => content is ToolApprovalResponseContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (content is not FunctionApprovalResponseContent approvalResponse)
if (content is not ToolApprovalResponseContent approvalResponse)
{
throw new InvalidOperationException("FunctionApprovalResponseEventGenerator only supports FunctionApprovalResponseContent.");
throw new InvalidOperationException("ToolApprovalResponseEventGenerator only supports ToolApprovalResponseContent.");
}
yield return new StreamingFunctionApprovalResponded
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
RequestId = approvalResponse.Id,
RequestId = approvalResponse.RequestId,
Approved = approvalResponse.Approved,
ItemId = idGenerator.GenerateMessageId()
};
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable OPENAI001 // Experimental OpenAI features
using System.ClientModel;
using OpenAI.Chat;
@@ -26,6 +26,7 @@ public static class OpenAIResponseClientExtensions
/// Creates an AI agent from an <see cref="ResponsesClient"/> using the OpenAI Response API.
/// </summary>
/// <param name="client">The <see cref="ResponsesClient" /> to use 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="instructions">Optional system instructions that define the agent's behavior and personality.</param>
/// <param name="name">Optional name for the agent for identification purposes.</param>
/// <param name="description">Optional description of the agent's capabilities and purpose.</param>
@@ -37,6 +38,7 @@ public static class OpenAIResponseClientExtensions
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> is <see langword="null"/>.</exception>
public static ChatClientAgent AsAIAgent(
this ResponsesClient client,
string? model = null,
string? instructions = null,
string? name = null,
string? description = null,
@@ -58,6 +60,7 @@ public static class OpenAIResponseClientExtensions
Tools = tools,
}
},
model,
clientFactory,
loggerFactory,
services);
@@ -68,6 +71,7 @@ public static class OpenAIResponseClientExtensions
/// </summary>
/// <param name="client">The <see cref="ResponsesClient" /> to use for the agent.</param>
/// <param name="options">Full set of options to configure 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="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
@@ -76,6 +80,7 @@ public static class OpenAIResponseClientExtensions
public static ChatClientAgent AsAIAgent(
this ResponsesClient client,
ChatClientAgentOptions options,
string? model = null,
Func<IChatClient, IChatClient>? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null)
@@ -83,7 +88,7 @@ public static class OpenAIResponseClientExtensions
Throw.IfNull(client);
Throw.IfNull(options);
var chatClient = client.AsIChatClient();
var chatClient = client.AsIChatClient(model);
if (clientFactory is not null)
{
@@ -100,6 +105,7 @@ public static class OpenAIResponseClientExtensions
/// This corresponds to setting the "store" property in the JSON representation to false.
/// </remarks>
/// <param name="responseClient">The client.</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="includeReasoningEncryptedContent">
/// Includes an encrypted version of reasoning tokens in reasoning item outputs.
/// This enables reasoning items to be used in multi-turn conversations when using the Responses API statelessly
@@ -109,10 +115,10 @@ public static class OpenAIResponseClientExtensions
/// <returns>An <see cref="IChatClient"/> that can be used to converse via the <see cref="ResponsesClient"/> that does not store responses for later retrieval.</returns>
/// <exception cref="ArgumentNullException"><paramref name="responseClient"/> is <see langword="null"/>.</exception>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public static IChatClient AsIChatClientWithStoredOutputDisabled(this ResponsesClient responseClient, bool includeReasoningEncryptedContent = true)
public static IChatClient AsIChatClientWithStoredOutputDisabled(this ResponsesClient responseClient, string? model = null, bool includeReasoningEncryptedContent = true)
{
return Throw.IfNull(responseClient)
.AsIChatClient()
.AsIChatClient(model)
.AsBuilder()
.ConfigureOptions(x => x.RawRepresentationFactory = _ => includeReasoningEncryptedContent
? new CreateResponseOptions() { StoredOutputEnabled = false, IncludedProperties = { IncludedResponseProperty.ReasoningEncryptedContent } }
@@ -10,8 +10,9 @@ using System.Runtime.CompilerServices;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Azure.Core;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
@@ -149,7 +150,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
agentName,
cancellationToken).ConfigureAwait(false);
targetAgent = agentRecord.Versions.Latest;
targetAgent = agentRecord.GetLatestVersion();
}
else
{
@@ -185,8 +185,8 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
private static void PopulateResultContent(McpServerToolResultContent resultContent, CallToolResult result)
{
// Ensure Output list is initialized
resultContent.Output ??= [];
// Ensure Outputs list is initialized
resultContent.Outputs ??= [];
if (result.IsError == true)
{
@@ -203,7 +203,7 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
}
}
resultContent.Output.Add(new TextContent($"Error: {errorText ?? "Unknown error from MCP Server call"}"));
resultContent.Outputs.Add(new TextContent($"Error: {errorText ?? "Unknown error from MCP Server call"}"));
return;
}
@@ -218,7 +218,7 @@ public sealed class DefaultMcpToolHandler : IMcpToolHandler, IAsyncDisposable
AIContent content = ConvertContentBlock(block);
if (content is not null)
{
resultContent.Output.Add(content);
resultContent.Outputs.Add(content);
}
}
}
@@ -150,7 +150,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, ResponseA
foreach (ChatMessage responseMessage in agentResponse.Messages)
{
if (responseMessage.Contents.Any(content => content is UserInputRequestContent))
if (responseMessage.Contents.Any(content => content is ToolApprovalRequestContent))
{
yield return responseMessage;
continue;
@@ -68,7 +68,7 @@ internal sealed class InvokeFunctionToolExecutor(
// If approval is required, add user input request content
if (requireApproval)
{
requestMessage.Contents.Add(new FunctionApprovalRequestContent(this.Id, functionCall));
requestMessage.Contents.Add(new ToolApprovalRequestContent(this.Id, functionCall));
}
AgentResponse agentResponse = new([requestMessage]);
@@ -85,7 +85,7 @@ internal sealed class InvokeMcpToolExecutor(
toolCall.AdditionalProperties.Add(headers);
}
McpServerToolApprovalRequestContent approvalRequest = new(this.Id, toolCall);
ToolApprovalRequestContent approvalRequest = new(this.Id, toolCall);
ChatMessage requestMessage = new(ChatRole.Assistant, [approvalRequest]);
AgentResponse agentResponse = new([requestMessage]);
@@ -127,11 +127,10 @@ internal sealed class InvokeMcpToolExecutor(
ExternalInputResponse response,
CancellationToken cancellationToken)
{
// Check for approval response
McpServerToolApprovalResponseContent? approvalResponse = response.Messages
ToolApprovalResponseContent? approvalResponse = response.Messages
.SelectMany(m => m.Contents)
.OfType<McpServerToolApprovalResponseContent>()
.FirstOrDefault(r => r.Id == this.Id);
.OfType<ToolApprovalResponseContent>()
.FirstOrDefault(r => r.RequestId == this.Id);
if (approvalResponse?.Approved != true)
{
@@ -174,7 +173,7 @@ internal sealed class InvokeMcpToolExecutor(
string? conversationId = this.GetConversationId();
await this.AssignResultAsync(context, resultContent).ConfigureAwait(false);
ChatMessage resultMessage = new(ChatRole.Tool, resultContent.Output);
ChatMessage resultMessage = new(ChatRole.Tool, resultContent.Outputs);
// Store messages if output path is configured
if (this.Model.Output?.Messages is not null)
@@ -192,20 +191,20 @@ internal sealed class InvokeMcpToolExecutor(
// Add messages to conversation if conversationId is provided
if (conversationId is not null)
{
ChatMessage assistantMessage = new(ChatRole.Assistant, resultContent.Output);
ChatMessage assistantMessage = new(ChatRole.Assistant, resultContent.Outputs);
await agentProvider.CreateMessageAsync(conversationId, assistantMessage, cancellationToken).ConfigureAwait(false);
}
}
private async ValueTask AssignResultAsync(IWorkflowContext context, McpServerToolResultContent toolResult)
{
if (this.Model.Output?.Result is null || toolResult.Output is null || toolResult.Output.Count == 0)
if (this.Model.Output?.Result is null || toolResult.Outputs is null || toolResult.Outputs.Count == 0)
{
return;
}
List<object?> parsedResults = [];
foreach (AIContent resultContent in toolResult.Output)
foreach (AIContent resultContent in toolResult.Outputs)
{
object? resultValue = resultContent switch
{
@@ -21,7 +21,7 @@ public sealed class AIAgentHostOptions
public bool EmitAgentResponseEvents { get; set; }
/// <summary>
/// Gets or sets a value indicating whether <see cref="UserInputRequestContent"/> should be intercepted and sent
/// Gets or sets a value indicating whether <see cref="ToolApprovalRequestContent"/> should be intercepted and sent
/// as a message to the workflow for handling, instead of being raised as a request.
/// </summary>
public bool InterceptUserInputRequests { get; set; }
@@ -19,7 +19,7 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
private AgentSession? _session;
private bool? _currentTurnEmitEvents;
private AIContentExternalHandler<UserInputRequestContent, UserInputResponseContent>? _userInputHandler;
private AIContentExternalHandler<ToolApprovalRequestContent, ToolApprovalResponseContent>? _userInputHandler;
private AIContentExternalHandler<FunctionCallContent, FunctionResultContent>? _functionCallHandler;
private static readonly ChatProtocolExecutorOptions s_defaultChatProtocolOptions = new()
@@ -38,7 +38,7 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
private ProtocolBuilder ConfigureUserInputHandling(ProtocolBuilder protocolBuilder)
{
this._userInputHandler = new AIContentExternalHandler<UserInputRequestContent, UserInputResponseContent>(
this._userInputHandler = new AIContentExternalHandler<ToolApprovalRequestContent, ToolApprovalResponseContent>(
ref protocolBuilder,
portId: $"{this.Id}_UserInput",
intercepted: this._options.InterceptUserInputRequests,
@@ -59,13 +59,13 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
}
private ValueTask HandleUserInputResponseAsync(
UserInputResponseContent response,
ToolApprovalResponseContent response,
IWorkflowContext context,
CancellationToken cancellationToken)
{
if (!this._userInputHandler!.MarkRequestAsHandled(response.Id))
if (!this._userInputHandler!.MarkRequestAsHandled(response.RequestId))
{
throw new InvalidOperationException($"No pending UserInputRequest found with id '{response.Id}'.");
throw new InvalidOperationException($"No pending ToolApprovalRequest found with id '{response.RequestId}'.");
}
List<ChatMessage> implicitTurnMessages = [new ChatMessage(ChatRole.User, [response])];
@@ -164,7 +164,7 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
private async ValueTask<AgentResponse> InvokeAgentAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, bool emitEvents, CancellationToken cancellationToken = default)
{
#pragma warning disable MEAI001
Dictionary<string, UserInputRequestContent> userInputRequests = new();
Dictionary<string, ToolApprovalRequestContent> userInputRequests = new();
Dictionary<string, FunctionCallContent> functionCalls = new();
AgentResponse response;
@@ -218,15 +218,15 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
{
foreach (AIContent content in contents)
{
if (content is UserInputRequestContent userInputRequest)
if (content is ToolApprovalRequestContent userInputRequest)
{
// It is an error to simultaneously have multiple outstanding user input requests with the same ID.
userInputRequests.Add(userInputRequest.Id, userInputRequest);
userInputRequests.Add(userInputRequest.RequestId, userInputRequest);
}
else if (content is UserInputResponseContent userInputResponse)
else if (content is ToolApprovalResponseContent userInputResponse)
{
// If the set of messages somehow already has a corresponding user input response, remove it.
_ = userInputRequests.Remove(userInputResponse.Id);
_ = userInputRequests.Remove(userInputResponse.RequestId);
}
else if (content is FunctionCallContent functionCall)
{
@@ -4,8 +4,9 @@
using System;
using System.Threading.Tasks;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
namespace Shared.Foundry;
@@ -306,8 +306,7 @@ internal sealed class WorkflowRunner
requestItem switch
{
FunctionCallContent functionCall when !functionCall.InformationalOnly => await InvokeFunctionAsync(functionCall).ConfigureAwait(false),
FunctionApprovalRequestContent functionApprovalRequest => ApproveFunction(functionApprovalRequest),
McpServerToolApprovalRequestContent mcpApprovalRequest => ApproveMCP(mcpApprovalRequest),
ToolApprovalRequestContent approvalRequest => ApproveToolCall(approvalRequest),
_ => HandleUnknown(requestItem),
};
@@ -325,16 +324,16 @@ internal sealed class WorkflowRunner
return null;
}
ChatMessage ApproveFunction(FunctionApprovalRequestContent functionApprovalRequest)
ChatMessage ApproveToolCall(ToolApprovalRequestContent approvalRequest)
{
Notify($"INPUT - Approving Function: {functionApprovalRequest.FunctionCall.Name}");
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]);
}
ChatMessage ApproveMCP(McpServerToolApprovalRequestContent mcpApprovalRequest)
{
Notify($"INPUT - Approving MCP: {mcpApprovalRequest.ToolCall.ToolName}");
return new ChatMessage(ChatRole.User, [mcpApprovalRequest.CreateResponse(approved: true)]);
string toolName = approvalRequest.ToolCall switch
{
McpServerToolCallContent mcp => mcp.Name,
FunctionCallContent f => f.Name,
_ => approvalRequest.ToolCall!.CallId
};
Notify($"INPUT - Approving: {toolName}");
return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(approved: true)]);
}
async Task<ChatMessage> InvokeFunctionAsync(FunctionCallContent functionCall)
@@ -5,7 +5,7 @@ using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.AI.Projects.Agents;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Files;
@@ -56,8 +56,8 @@ public class AIProjectClientCreateTests
var agentRecord = await this._client.Agents.GetAgentAsync(agent.Name);
Assert.NotNull(agentRecord);
Assert.Equal(AgentName, agentRecord.Value.Name);
var definition = Assert.IsType<PromptAgentDefinition>(agentRecord.Value.Versions.Latest.Definition);
Assert.Equal(AgentDescription, agentRecord.Value.Versions.Latest.Description);
var definition = Assert.IsType<PromptAgentDefinition>(agentRecord.Value.GetLatestVersion().Definition);
Assert.Equal(AgentDescription, agentRecord.Value.GetLatestVersion().Description);
Assert.Equal(AgentInstructions, definition.Instructions);
}
finally
@@ -6,8 +6,8 @@ using System.Linq;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Responses;

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