mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feat/durable_task
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj",
|
||||
"src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj",
|
||||
"src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj",
|
||||
"src\\Microsoft.Agents.AI.FoundryMemory\\Microsoft.Agents.AI.FoundryMemory.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
|
||||
|
||||
@@ -4,7 +4,6 @@ using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
@@ -27,7 +26,7 @@ ChatClient chatClient = new AzureOpenAIClient(
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
AIAgent agent = chatClient.AsIChatClient().AsAIAgent(
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -82,7 +82,7 @@ ChatClient chatClient = new AzureOpenAIClient(
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
ChatClientAgent agent = chatClient.AsIChatClient().AsAIAgent(
|
||||
ChatClientAgent agent = chatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant with access to restaurant information.",
|
||||
tools: tools);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,7 +4,6 @@ using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
@@ -27,7 +26,7 @@ ChatClient chatClient = new AzureOpenAIClient(
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
AIAgent agent = chatClient.AsIChatClient().AsAIAgent(
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -60,7 +60,7 @@ ChatClient openAIChatClient = new AzureOpenAIClient(
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
ChatClientAgent baseAgent = openAIChatClient.AsIChatClient().AsAIAgent(
|
||||
ChatClientAgent baseAgent = openAIChatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant in charge of approving expenses",
|
||||
tools: tools);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,7 +4,6 @@ using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenAI.Chat;
|
||||
using RecipeAssistant;
|
||||
@@ -37,7 +36,7 @@ ChatClient chatClient = new AzureOpenAIClient(
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
AIAgent baseAgent = chatClient.AsIChatClient().AsAIAgent(
|
||||
AIAgent baseAgent = chatClient.AsAIAgent(
|
||||
name: "RecipeAgent",
|
||||
instructions: """
|
||||
You are a helpful recipe assistant. When users ask you to create or suggest a recipe,
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+72
-56
@@ -12,11 +12,8 @@ using OpenAI.Responses;
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// Memory store configuration
|
||||
// NOTE: Memory stores must be created beforehand via Azure Portal or Python SDK.
|
||||
// The .NET SDK currently only supports using existing memory stores with agents.
|
||||
string memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? throw new InvalidOperationException("AZURE_AI_MEMORY_STORE_ID is not set.");
|
||||
string embeddingModelName = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
|
||||
string memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? $"foundry-memory-sample-{Guid.NewGuid():N}";
|
||||
|
||||
const string AgentInstructions = """
|
||||
You are a helpful assistant that remembers past conversations.
|
||||
@@ -32,71 +29,57 @@ const string AgentNameNative = "MemorySearchAgent-NATIVE";
|
||||
string userScope = $"user_{Environment.MachineName}";
|
||||
|
||||
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
|
||||
DefaultAzureCredential credential = new();
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
// Ensure the memory store exists and has memories to retrieve.
|
||||
await EnsureMemoryStoreAsync();
|
||||
|
||||
// Create the Memory Search tool configuration
|
||||
MemorySearchPreviewTool memorySearchTool = new(memoryStoreName, userScope)
|
||||
{
|
||||
// Optional: Configure how quickly new memories are indexed (in seconds)
|
||||
UpdateDelay = 1,
|
||||
|
||||
// Optional: Configure search behavior
|
||||
SearchOptions = new MemorySearchToolOptions
|
||||
{
|
||||
// Additional search options can be configured here if needed
|
||||
}
|
||||
};
|
||||
MemorySearchPreviewTool memorySearchTool = new(memoryStoreName, userScope) { UpdateDelay = 0 };
|
||||
|
||||
// Create agent using Option 1 (MEAI) or Option 2 (Native SDK)
|
||||
AIAgent agent = await CreateAgentWithMEAI();
|
||||
// AIAgent agent = await CreateAgentWithNativeSDK();
|
||||
|
||||
Console.WriteLine("Agent created with Memory Search tool. Starting conversation...\n");
|
||||
|
||||
// Conversation 1: Share some personal information
|
||||
Console.WriteLine("User: My name is Alice and I love programming in C#.");
|
||||
AgentResponse response1 = await agent.RunAsync("My name is Alice and I love programming in C#.");
|
||||
Console.WriteLine($"Agent: {response1.Messages.LastOrDefault()?.Text}\n");
|
||||
|
||||
// Allow time for memory to be indexed
|
||||
await Task.Delay(2000);
|
||||
|
||||
// Conversation 2: Test if the agent remembers
|
||||
Console.WriteLine("User: What's my name and what programming language do I prefer?");
|
||||
AgentResponse response2 = await agent.RunAsync("What's my name and what programming language do I prefer?");
|
||||
Console.WriteLine($"Agent: {response2.Messages.LastOrDefault()?.Text}\n");
|
||||
|
||||
// Inspect memory search results if available in raw response items
|
||||
// Note: Memory search tool call results appear as AgentResponseItem types
|
||||
foreach (var message in response2.Messages)
|
||||
try
|
||||
{
|
||||
if (message.RawRepresentation is AgentResponseItem agentResponseItem &&
|
||||
agentResponseItem is MemorySearchToolCallResponseItem memorySearchResult)
|
||||
{
|
||||
Console.WriteLine($"Memory Search Status: {memorySearchResult.Status}");
|
||||
Console.WriteLine($"Memory Search Results Count: {memorySearchResult.Results.Count}");
|
||||
Console.WriteLine("Agent created with Memory Search tool. Starting conversation...\n");
|
||||
|
||||
foreach (var result in memorySearchResult.Results)
|
||||
// The agent uses the memory search tool to recall stored information.
|
||||
Console.WriteLine("User: What's my name and what programming language do I prefer?");
|
||||
AgentResponse response = await agent.RunAsync("What's my name and what programming language do I prefer?");
|
||||
Console.WriteLine($"Agent: {response.Messages.LastOrDefault()?.Text}\n");
|
||||
|
||||
// Inspect memory search results if available in raw response items.
|
||||
foreach (var message in response.Messages)
|
||||
{
|
||||
if (message.RawRepresentation is MemorySearchToolCallResponseItem memorySearchResult)
|
||||
{
|
||||
var memoryItem = result.MemoryItem;
|
||||
Console.WriteLine($" - Memory ID: {memoryItem.MemoryId}");
|
||||
Console.WriteLine($" Scope: {memoryItem.Scope}");
|
||||
Console.WriteLine($" Content: {memoryItem.Content}");
|
||||
Console.WriteLine($" Updated: {memoryItem.UpdatedAt}");
|
||||
Console.WriteLine($"Memory Search Status: {memorySearchResult.Status}");
|
||||
Console.WriteLine($"Memory Search Results Count: {memorySearchResult.Results.Count}");
|
||||
|
||||
foreach (var result in memorySearchResult.Results)
|
||||
{
|
||||
var memoryItem = result.MemoryItem;
|
||||
Console.WriteLine($" - Memory ID: {memoryItem.MemoryId}");
|
||||
Console.WriteLine($" Scope: {memoryItem.Scope}");
|
||||
Console.WriteLine($" Content: {memoryItem.Content}");
|
||||
Console.WriteLine($" Updated: {memoryItem.UpdatedAt}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup: Delete the agent and memory store.
|
||||
Console.WriteLine("\nCleaning up...");
|
||||
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
|
||||
Console.WriteLine("Agent deleted.");
|
||||
await aiProjectClient.MemoryStores.DeleteMemoryStoreAsync(memoryStoreName);
|
||||
Console.WriteLine("Memory store deleted.");
|
||||
}
|
||||
|
||||
// Cleanup: Delete the agent (memory store persists and should be cleaned up separately if needed)
|
||||
Console.WriteLine("\nCleaning up agent...");
|
||||
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
|
||||
Console.WriteLine("Agent deleted successfully.");
|
||||
|
||||
// NOTE: Memory stores are long-lived resources and are NOT deleted with the agent.
|
||||
// To delete a memory store, use the Azure Portal or Python SDK:
|
||||
// await project_client.memory_stores.delete(memory_store.name)
|
||||
|
||||
// --- Agent Creation Options ---
|
||||
#pragma warning disable CS8321 // Local function is declared but never used
|
||||
|
||||
// Option 1 - Using MemorySearchTool wrapped as MEAI AITool
|
||||
@@ -122,3 +105,36 @@ async Task<AIAgent> CreateAgentWithNativeSDK()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Helpers — kept at the bottom so the main agent flow above stays clean.
|
||||
async Task EnsureMemoryStoreAsync()
|
||||
{
|
||||
Console.WriteLine($"Creating memory store '{memoryStoreName}'...");
|
||||
try
|
||||
{
|
||||
await aiProjectClient.MemoryStores.GetMemoryStoreAsync(memoryStoreName);
|
||||
Console.WriteLine("Memory store already exists.");
|
||||
}
|
||||
catch (System.ClientModel.ClientResultException ex) when (ex.Status == 404)
|
||||
{
|
||||
MemoryStoreDefaultDefinition definition = new(deploymentName, embeddingModelName);
|
||||
await aiProjectClient.MemoryStores.CreateMemoryStoreAsync(memoryStoreName, definition, "Sample memory store for Memory Search demo");
|
||||
Console.WriteLine("Memory store created.");
|
||||
}
|
||||
|
||||
Console.WriteLine("Storing memories from a prior conversation...");
|
||||
MemoryUpdateOptions memoryOptions = new(userScope) { UpdateDelay = 0 };
|
||||
memoryOptions.Items.Add(ResponseItem.CreateUserMessageItem("My name is Alice and I love programming in C#."));
|
||||
|
||||
MemoryUpdateResult updateResult = await aiProjectClient.MemoryStores.WaitForMemoriesUpdateAsync(
|
||||
memoryStoreName: memoryStoreName,
|
||||
options: memoryOptions,
|
||||
pollingInterval: 500);
|
||||
|
||||
if (updateResult.Status == MemoryStoreUpdateStatus.Failed)
|
||||
{
|
||||
throw new InvalidOperationException($"Memory update failed: {updateResult.ErrorDetails}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"Memory update completed (status: {updateResult.Status}).\n");
|
||||
}
|
||||
|
||||
+8
-8
@@ -10,7 +10,7 @@ using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ChatClient = OpenAI.Chat.ChatClient;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
@@ -36,7 +36,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
return chatClient.AsAIAgent(
|
||||
name: "AgenticChat",
|
||||
description: "A simple chat agent using Azure OpenAI");
|
||||
}
|
||||
@@ -45,7 +45,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
return chatClient.AsAIAgent(
|
||||
name: "BackendToolRenderer",
|
||||
description: "An agent that can render backend tools using Azure OpenAI",
|
||||
tools: [AIFunctionFactory.Create(
|
||||
@@ -59,7 +59,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
return chatClient.AsAIAgent(
|
||||
name: "HumanInTheLoopAgent",
|
||||
description: "An agent that involves human feedback in its decision-making process using Azure OpenAI");
|
||||
}
|
||||
@@ -68,7 +68,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().AsAIAgent(
|
||||
return chatClient.AsAIAgent(
|
||||
name: "ToolBasedGenerativeUIAgent",
|
||||
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
|
||||
}
|
||||
@@ -76,7 +76,7 @@ internal static class ChatClientAgentFactory
|
||||
public static AIAgent CreateAgenticUI(JsonSerializerOptions options)
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
|
||||
var baseAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "AgenticUIAgent",
|
||||
Description = "An agent that generates agentic user interfaces using Azure OpenAI",
|
||||
@@ -119,7 +119,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(
|
||||
var baseAgent = chatClient.AsAIAgent(
|
||||
name: "SharedStateAgent",
|
||||
description: "An agent that demonstrates shared state patterns using Azure OpenAI");
|
||||
|
||||
@@ -130,7 +130,7 @@ internal static class ChatClientAgentFactory
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
|
||||
var baseAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "PredictiveStateUpdatesAgent",
|
||||
Description = "An agent that demonstrates predictive state updates using Azure OpenAI",
|
||||
|
||||
@@ -74,7 +74,7 @@ AzureOpenAIClient azureOpenAIClient = new AzureOpenAIClient(
|
||||
ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName);
|
||||
|
||||
// Create AI agent
|
||||
ChatClientAgent agent = chatClient.AsIChatClient().AsAIAgent(
|
||||
ChatClientAgent agent = chatClient.AsAIAgent(
|
||||
name: "ChatAssistant",
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
@@ -162,7 +162,7 @@ dotnet run
|
||||
Edit the instructions in `Server/Program.cs`:
|
||||
|
||||
```csharp
|
||||
ChatClientAgent agent = chatClient.AsIChatClient().AsAIAgent(
|
||||
ChatClientAgent agent = chatClient.AsAIAgent(
|
||||
name: "ChatAssistant",
|
||||
instructions: "You are a helpful coding assistant specializing in C# and .NET.");
|
||||
```
|
||||
|
||||
@@ -6,7 +6,6 @@ using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
@@ -28,7 +27,7 @@ AzureOpenAIClient azureOpenAIClient = new(
|
||||
|
||||
ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName);
|
||||
|
||||
ChatClientAgent agent = chatClient.AsIChatClient().AsAIAgent(
|
||||
ChatClientAgent agent = chatClient.AsAIAgent(
|
||||
name: "ChatAssistant",
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -89,7 +90,6 @@ builder.Services.AddSingleton<AIAgent>(sp =>
|
||||
|
||||
return new OpenAIClient(apiKey)
|
||||
.GetChatClient(model)
|
||||
.AsIChatClient()
|
||||
.AsAIAgent(
|
||||
name: "ExpenseApprovalAgent",
|
||||
instructions: "You are an expense approval assistant. You can list pending expenses "
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
<PropertyGroup>
|
||||
<!-- Disable packing until we are ready to release this as a nuget -->
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#pragma warning disable CS0618 // Type or member is obsolete - Internal use of obsolete types for backward compatibility
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@@ -133,7 +134,25 @@ internal sealed class ExecutorProtocol(MessageRouter router, ISet<Type> sendType
|
||||
|
||||
public bool CanHandle(Type type) => router.CanHandle(type);
|
||||
|
||||
public bool CanOutput(Type type) => this._yieldTypes.Contains(new(type));
|
||||
private readonly ConcurrentDictionary<Type, bool> _canOutputCache = new();
|
||||
|
||||
public bool CanOutput(Type type)
|
||||
{
|
||||
return this._canOutputCache.GetOrAdd(type, this.CanOutputCore);
|
||||
}
|
||||
|
||||
private bool CanOutputCore(Type type)
|
||||
{
|
||||
foreach (TypeId yieldType in this._yieldTypes)
|
||||
{
|
||||
if (yieldType.IsMatchPolymorphic(type))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ProtocolDescriptor Describe() => new(this.Router.IncomingTypes, yieldTypes, sendTypes, this.Router.HasCatchAll);
|
||||
}
|
||||
|
||||
+4
-2
@@ -14,6 +14,8 @@ namespace AzureAIAgentsPersistent.IntegrationTests;
|
||||
|
||||
public class AzureAIAgentsPersistentCreateTests
|
||||
{
|
||||
private const string SkipCodeInterpreterReason = "Azure AI Code Interpreter intermittently fails to execute uploaded files in CI";
|
||||
|
||||
private readonly PersistentAgentsClient _persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
[Theory]
|
||||
@@ -131,11 +133,11 @@ public class AzureAIAgentsPersistentCreateTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = SkipCodeInterpreterReason)]
|
||||
public Task CreateAgent_CreatesAgentWithCodeInterpreter_ChatClientAgentOptionsAsync()
|
||||
=> this.CreateAgent_CreatesAgentWithCodeInterpreterAsync("CreateWithChatClientAgentOptionsAsync");
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
[Fact(Skip = SkipCodeInterpreterReason)]
|
||||
public Task CreateAgent_CreatesAgentWithCodeInterpreter_FoundryOptionsAsync()
|
||||
=> this.CreateAgent_CreatesAgentWithCodeInterpreterAsync("CreateWithFoundryOptionsAsync");
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for polymorphic output type handling in workflows.
|
||||
/// Verifies that executors can return derived types when the declared output type is a base class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This addresses GitHub issue #4134: InvalidOperationException when returning derived type as workflow output.
|
||||
/// </remarks>
|
||||
public partial class PolymorphicOutputTests
|
||||
{
|
||||
#region Test Type Hierarchy
|
||||
|
||||
/// <summary>
|
||||
/// Base class used as declared output type.
|
||||
/// </summary>
|
||||
public class BaseOutput
|
||||
{
|
||||
public virtual string Name => "BaseOutput";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derived class returned at runtime.
|
||||
/// </summary>
|
||||
public class DerivedOutput : BaseOutput
|
||||
{
|
||||
public override string Name => "DerivedOutput";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Second-level derived class for testing multiple inheritance levels.
|
||||
/// </summary>
|
||||
public class GrandchildOutput : DerivedOutput
|
||||
{
|
||||
public override string Name => "GrandchildOutput";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unrelated class that should NOT be accepted as output.
|
||||
/// </summary>
|
||||
public class UnrelatedOutput
|
||||
{
|
||||
public string Name => "UnrelatedOutput";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Executors
|
||||
|
||||
/// <summary>
|
||||
/// Executor that declares BaseOutput as yield type but returns DerivedOutput.
|
||||
/// </summary>
|
||||
internal sealed class DerivedOutputExecutor() : Executor(nameof(DerivedOutputExecutor))
|
||||
{
|
||||
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
||||
{
|
||||
return protocolBuilder.ConfigureRoutes(routeBuilder =>
|
||||
routeBuilder.AddHandler<string, BaseOutput>(this.HandleAsync));
|
||||
}
|
||||
|
||||
private async ValueTask<BaseOutput> HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(10, cancellationToken);
|
||||
|
||||
// Arrange: Return a derived type where the method signature declares the base type
|
||||
return new DerivedOutput();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that declares BaseOutput as yield type but returns GrandchildOutput (two levels deep).
|
||||
/// </summary>
|
||||
internal sealed class GrandchildOutputExecutor() : Executor(nameof(GrandchildOutputExecutor))
|
||||
{
|
||||
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
||||
{
|
||||
return protocolBuilder.ConfigureRoutes(routeBuilder =>
|
||||
routeBuilder.AddHandler<string, BaseOutput>(this.HandleAsync));
|
||||
}
|
||||
|
||||
private async ValueTask<BaseOutput> HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(10, cancellationToken);
|
||||
|
||||
// Arrange: Return a grandchild type (two inheritance levels)
|
||||
return new GrandchildOutput();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that attempts to return an unrelated type - should fail validation.
|
||||
/// This executor intentionally bypasses type safety to test runtime validation.
|
||||
/// </summary>
|
||||
internal sealed class UnrelatedOutputExecutor() : Executor(nameof(UnrelatedOutputExecutor))
|
||||
{
|
||||
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
||||
{
|
||||
return protocolBuilder.ConfigureRoutes(routeBuilder =>
|
||||
routeBuilder.AddHandler<string, BaseOutput>(this.HandleAsync));
|
||||
}
|
||||
|
||||
private async ValueTask<BaseOutput> HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Arrange: Attempt to yield an unrelated type - should throw
|
||||
UnrelatedOutput unrelated = new();
|
||||
await context.YieldOutputAsync(unrelated, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// This line should not be reached
|
||||
return new BaseOutput();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that returns the exact declared type (baseline test).
|
||||
/// </summary>
|
||||
internal sealed class ExactTypeExecutor() : Executor(nameof(ExactTypeExecutor))
|
||||
{
|
||||
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
||||
{
|
||||
return protocolBuilder.ConfigureRoutes(routeBuilder =>
|
||||
routeBuilder.AddHandler<string, BaseOutput>(this.HandleAsync));
|
||||
}
|
||||
|
||||
private ValueTask<BaseOutput> HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
BaseOutput result = new();
|
||||
return new ValueTask<BaseOutput>(result);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that returning a derived type when the declared output type is a base class succeeds.
|
||||
/// This is the main regression test for GitHub issue #4134.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReturningDerivedType_WhenBaseTypeIsDeclared_ShouldSucceedAsync()
|
||||
{
|
||||
// Arrange
|
||||
DerivedOutputExecutor executor = new();
|
||||
WorkflowBuilder builder = new WorkflowBuilder(executor).WithOutputFrom(executor);
|
||||
Workflow workflow = builder.Build();
|
||||
|
||||
// Act
|
||||
List<WorkflowEvent> events = [];
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, "test input");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
events.Should().NotBeEmpty("workflow should produce events");
|
||||
|
||||
List<WorkflowOutputEvent> outputEvents = events.OfType<WorkflowOutputEvent>().ToList();
|
||||
outputEvents.Should().ContainSingle("workflow should produce exactly one output event");
|
||||
|
||||
WorkflowOutputEvent outputEvent = outputEvents.Single();
|
||||
outputEvent.Data.Should().BeOfType<DerivedOutput>("output should be the derived type");
|
||||
((DerivedOutput)outputEvent.Data!).Name.Should().Be("DerivedOutput");
|
||||
|
||||
// Verify no error events
|
||||
List<WorkflowErrorEvent> errorEvents = events.OfType<WorkflowErrorEvent>().ToList();
|
||||
errorEvents.Should().BeEmpty("workflow should not produce error events");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that returning a grandchild type (multiple inheritance levels) succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReturningGrandchildType_WhenBaseTypeIsDeclared_ShouldSucceedAsync()
|
||||
{
|
||||
// Arrange
|
||||
GrandchildOutputExecutor executor = new();
|
||||
WorkflowBuilder builder = new WorkflowBuilder(executor).WithOutputFrom(executor);
|
||||
Workflow workflow = builder.Build();
|
||||
|
||||
// Act
|
||||
List<WorkflowEvent> events = [];
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, "test input");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
events.Should().NotBeEmpty("workflow should produce events");
|
||||
|
||||
List<WorkflowOutputEvent> outputEvents = events.OfType<WorkflowOutputEvent>().ToList();
|
||||
outputEvents.Should().ContainSingle("workflow should produce exactly one output event");
|
||||
|
||||
WorkflowOutputEvent outputEvent = outputEvents.Single();
|
||||
outputEvent.Data.Should().BeOfType<GrandchildOutput>("output should be the grandchild type");
|
||||
((GrandchildOutput)outputEvent.Data!).Name.Should().Be("GrandchildOutput");
|
||||
|
||||
// Verify no error events
|
||||
List<WorkflowErrorEvent> errorEvents = events.OfType<WorkflowErrorEvent>().ToList();
|
||||
errorEvents.Should().BeEmpty("workflow should not produce error events");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that returning an unrelated type still throws InvalidOperationException.
|
||||
/// This ensures the fix doesn't break the existing validation for truly incompatible types.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReturningUnrelatedType_WhenBaseTypeIsDeclared_ShouldFailAsync()
|
||||
{
|
||||
// Arrange
|
||||
UnrelatedOutputExecutor executor = new();
|
||||
WorkflowBuilder builder = new WorkflowBuilder(executor).WithOutputFrom(executor);
|
||||
Workflow workflow = builder.Build();
|
||||
|
||||
// Act
|
||||
List<WorkflowEvent> events = [];
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, "test input");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert: Should have an error event with InvalidOperationException message
|
||||
List<WorkflowErrorEvent> errorEvents = events.OfType<WorkflowErrorEvent>().ToList();
|
||||
errorEvents.Should().ContainSingle("workflow should produce exactly one error event");
|
||||
|
||||
WorkflowErrorEvent errorEvent = errorEvents.Single();
|
||||
string errorMessage = errorEvent.Data?.ToString() ?? string.Empty;
|
||||
errorMessage.Should().Contain("Cannot output object of type UnrelatedOutput");
|
||||
errorMessage.Should().Contain("BaseOutput");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that returning the exact declared type still works (baseline test).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReturningExactType_WhenSameTypeIsDeclared_ShouldSucceedAsync()
|
||||
{
|
||||
// Arrange: Create an executor that returns the exact declared type
|
||||
ExactTypeExecutor executor = new();
|
||||
WorkflowBuilder builder = new WorkflowBuilder(executor).WithOutputFrom(executor);
|
||||
Workflow workflow = builder.Build();
|
||||
|
||||
// Act
|
||||
List<WorkflowEvent> events = [];
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, "test input");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
events.Should().NotBeEmpty("workflow should produce events");
|
||||
|
||||
List<WorkflowOutputEvent> outputEvents = events.OfType<WorkflowOutputEvent>().ToList();
|
||||
outputEvents.Should().ContainSingle("workflow should produce exactly one output event");
|
||||
|
||||
WorkflowOutputEvent outputEvent = outputEvents.Single();
|
||||
outputEvent.Data.Should().BeOfType<BaseOutput>("output should be the exact base type");
|
||||
|
||||
// Verify no error events
|
||||
List<WorkflowErrorEvent> errorEvents = events.OfType<WorkflowErrorEvent>().ToList();
|
||||
errorEvents.Should().BeEmpty("workflow should not produce error events");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+3
-1
@@ -19,6 +19,8 @@ namespace OpenAIAssistant.IntegrationTests;
|
||||
|
||||
public class OpenAIAssistantClientExtensionsTests
|
||||
{
|
||||
private const string SkipCodeInterpreterReason = "OpenAI Assistant Code Interpreter intermittently fails in CI";
|
||||
|
||||
private readonly AssistantClient _assistantClient = new OpenAIClient(TestConfiguration.GetRequiredValue(TestSettings.OpenAIApiKey)).GetAssistantClient();
|
||||
private readonly OpenAIFileClient _fileClient = new OpenAIClient(TestConfiguration.GetRequiredValue(TestSettings.OpenAIApiKey)).GetOpenAIFileClient();
|
||||
|
||||
@@ -81,7 +83,7 @@ public class OpenAIAssistantClientExtensionsTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Theory(Skip = SkipCodeInterpreterReason)]
|
||||
[InlineData("CreateWithChatClientAgentOptionsAsync")]
|
||||
[InlineData("CreateWithChatClientAgentOptionsSync")]
|
||||
[InlineData("CreateWithParamsAsync")]
|
||||
|
||||
Reference in New Issue
Block a user