// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics; using System.Reflection; using Microsoft.Agents.AI.DurableTask.State; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Configuration; using OpenAI.Chat; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; /// /// Tests for scenarios where an external client interacts with Durable Task Agents. /// [Collection("Sequential")] [Trait("Category", "Integration")] public sealed class AgentEntityTests(ITestOutputHelper outputHelper) : IDisposable { private static readonly TimeSpan s_defaultTimeout = Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(30); private static readonly IConfiguration s_configuration = new ConfigurationBuilder() .AddUserSecrets(Assembly.GetExecutingAssembly()) .AddEnvironmentVariables() .Build(); private readonly ITestOutputHelper _outputHelper = outputHelper; private readonly CancellationTokenSource _cts = new(delay: s_defaultTimeout); private CancellationToken TestTimeoutToken => this._cts.Token; public void Dispose() => this._cts.Dispose(); [Fact] public async Task EntityNamePrefixAsync() { // Setup AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).AsAIAgent( name: "TestAgent", instructions: "You are a helpful assistant that always responds with a friendly greeting." ); using TestHelper testHelper = TestHelper.Start([simpleAgent], this._outputHelper); // A proxy agent is needed to call the hosted test agent AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); AgentSession session = await simpleAgentProxy.CreateSessionAsync(this.TestTimeoutToken); DurableTaskClient client = testHelper.GetClient(); AgentSessionId sessionId = session.GetService(); EntityInstanceId expectedEntityId = new($"dafx-{simpleAgent.Name}", sessionId.Key); EntityMetadata? entity = await client.Entities.GetEntityAsync(expectedEntityId, false, this.TestTimeoutToken); Assert.Null(entity); // Act: send a prompt to the agent await simpleAgentProxy.RunAsync( message: "Hello!", session, cancellationToken: this.TestTimeoutToken); // Assert: verify the agent state was stored with the correct entity name prefix entity = await client.Entities.GetEntityAsync(expectedEntityId, true, this.TestTimeoutToken); Assert.NotNull(entity); Assert.True(entity.IncludesState); DurableAgentState state = entity.State.ReadAs(); DurableAgentStateRequest request = Assert.Single(state.Data.ConversationHistory.OfType()); Assert.Null(request.OrchestrationId); } [Theory] [InlineData("run")] [InlineData("Run")] [InlineData("RunAgentAsync")] public async Task RunAgentMethodNamesAllWorkAsync(string runAgentMethodName) { // Setup AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).AsAIAgent( name: "TestAgent", instructions: "You are a helpful assistant that always responds with a friendly greeting." ); using TestHelper testHelper = TestHelper.Start([simpleAgent], this._outputHelper); // A proxy agent is needed to call the hosted test agent AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); AgentSession session = await simpleAgentProxy.CreateSessionAsync(this.TestTimeoutToken); DurableTaskClient client = testHelper.GetClient(); AgentSessionId sessionId = session.GetService(); EntityInstanceId expectedEntityId = new($"dafx-{simpleAgent.Name}", sessionId.Key); EntityMetadata? entity = await client.Entities.GetEntityAsync(expectedEntityId, false, this.TestTimeoutToken); Assert.Null(entity); // Act: send a prompt to the agent await client.Entities.SignalEntityAsync( expectedEntityId, runAgentMethodName, new RunRequest("Hello!"), cancellation: this.TestTimeoutToken); while (!this.TestTimeoutToken.IsCancellationRequested) { await Task.Delay(500, this.TestTimeoutToken); // Assert: verify the agent state was stored with the correct entity name prefix entity = await client.Entities.GetEntityAsync(expectedEntityId, true, this.TestTimeoutToken); if (entity is not null) { break; } } Assert.NotNull(entity); Assert.True(entity.IncludesState); DurableAgentState state = entity.State.ReadAs(); DurableAgentStateRequest request = Assert.Single(state.Data.ConversationHistory.OfType()); Assert.Null(request.OrchestrationId); } [Fact] public async Task OrchestrationIdSetDuringOrchestrationAsync() { // Arrange AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).AsAIAgent( name: "TestAgent", instructions: "You are a helpful assistant that always responds with a friendly greeting." ); using TestHelper testHelper = TestHelper.Start( [simpleAgent], this._outputHelper, registry => registry.AddOrchestrator()); DurableTaskClient client = testHelper.GetClient(); // Act string orchestrationId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(TestOrchestrator), "What is the capital of Maine?"); OrchestrationMetadata? status = await client.WaitForInstanceCompletionAsync( orchestrationId, true, this.TestTimeoutToken); // Assert EntityInstanceId expectedEntityId = AgentSessionId.Parse(status.ReadOutputAs()!); EntityMetadata? entity = await client.Entities.GetEntityAsync(expectedEntityId, true, this.TestTimeoutToken); Assert.NotNull(entity); Assert.True(entity.IncludesState); DurableAgentState state = entity.State.ReadAs(); DurableAgentStateRequest request = Assert.Single(state.Data.ConversationHistory.OfType()); Assert.Equal(orchestrationId, request.OrchestrationId); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Constructed via reflection.")] private sealed class TestOrchestrator : TaskOrchestrator { public override async Task RunAsync(TaskOrchestrationContext context, string input) { DurableAIAgent writer = context.GetAgent("TestAgent"); AgentSession writerSession = await writer.CreateSessionAsync(); await writer.RunAsync( message: context.GetInput()!, session: writerSession); AgentSessionId sessionId = writerSession.GetService(); return sessionId.ToString(); } } }