.NET: Add AgentWorkflowBuilder group chat (#861)

* Add AgentWorkflowBuilder group chat

And fix a variety of issues along the way:
- Use DateTime{Offset}.UtcNow rather than Now
- AIAgentHostExecutor shouldn't be publishing empty messages
- Sequential workflows should be flowing all history and not just the output from the previous agent as the input into the next agent
- Renamed some of the new agent workflow methods... still not super happy with the shape, though
- Simplified handoffs builder, e.g. using a hashset with a custom comparer instead of a dictionary
- Improved multi-service use by trying to change assistant->user role for messages created by other agents
- Changed MessageMerger to rely on M.E.AI's coalescing more and to avoid empty contents / text
- Ensured that messages from ChatClientAgent include MessageId and CreatedAt timestamps
- Avoided including instructions for agents in a handoff workflow that don't have any handoffs
- Removed the unnecessary end function in handoffs
- Improved naming of executors to include agent name for debuggability
- Use "N" formatting with Guid.ToString everywhere, to avoid the unnecessary extra dash character which is also not valid in various places (like function tool names)
- Replace `params T[]` with `params IEnumerable<T>` to make public APIs more flexible in what they consume

* Address feedback

- Fix unintentional provider change in sample
This commit is contained in:
Stephen Toub
2025-09-24 16:38:34 +00:00
committed by GitHub
parent 7c70c33157
commit 03ef7f054f
51 changed files with 888 additions and 619 deletions
@@ -30,7 +30,7 @@ public class CosmosActorStateStorageConcurrencyTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key = "testKey";
var value1 = JsonSerializer.SerializeToElement("value1");
@@ -69,7 +69,7 @@ public class CosmosActorStateStorageConcurrencyTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
// Setup initial state
var initialOperations = new List<ActorStateWriteOperation>
@@ -184,7 +184,7 @@ public class CosmosActorStateStorageConcurrencyTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
@@ -200,7 +200,7 @@ public class CosmosActorStateStorageConcurrencyTests
Assert.NotEmpty(resultWithNullETag.ETag);
// Clean up for next test
var uniqueActorId1 = new ActorId("TestActor", Guid.NewGuid().ToString());
var uniqueActorId1 = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
// Act & Assert - Test empty eTag (should create new document)
var resultWithEmptyETag = await storage.WriteStateAsync(uniqueActorId1, operations, string.Empty, cancellationToken);
@@ -209,7 +209,7 @@ public class CosmosActorStateStorageConcurrencyTests
Assert.NotEmpty(resultWithEmptyETag.ETag);
// Clean up for next test
var uniqueActorId2 = new ActorId("TestActor", Guid.NewGuid().ToString());
var uniqueActorId2 = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
// Act & Assert - Test "0" initial eTag (should create new document)
var resultWithInitialETag = await storage.WriteStateAsync(uniqueActorId2, operations, "0", cancellationToken);
@@ -255,7 +255,7 @@ public class CosmosActorStateStorageConcurrencyTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Fresh actor
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); // Fresh actor
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
@@ -305,7 +305,7 @@ public class CosmosActorStateStorageConcurrencyTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Non-existent actor
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N")); // Non-existent actor
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
@@ -27,7 +27,7 @@ public class CosmosActorStateStorageListKeysTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string PrefixKey1 = "prefix_key1";
const string PrefixKey2 = "prefix_key2";
@@ -70,7 +70,7 @@ public class CosmosActorStateStorageListKeysTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key1 = "key1";
const string Key2 = "key2";
@@ -108,7 +108,7 @@ public class CosmosActorStateStorageListKeysTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
// Act - List keys for actor with no state
var readOperations = new List<ActorStateReadOperation>
@@ -133,7 +133,7 @@ public class CosmosActorStateStorageListKeysTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key1 = "key1";
const string Key2 = "key2";
@@ -174,7 +174,7 @@ public class CosmosActorStateStorageListKeysTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key1 = "key1";
const string Key2 = "key2";
@@ -226,7 +226,7 @@ public class CosmosActorStateStorageListKeysTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
// Create keys with different prefixes
string[] userKeys = ["user_profile", "user_settings", "user_preferences"];
@@ -27,7 +27,7 @@ public class CosmosActorStateStorageTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
@@ -53,7 +53,7 @@ public class CosmosActorStateStorageTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key1 = "key1";
const string Key2 = "key2";
@@ -155,7 +155,7 @@ public class CosmosActorStateStorageTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
@@ -197,8 +197,8 @@ public class CosmosActorStateStorageTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId1 = new ActorId("TestActor1", Guid.NewGuid().ToString());
var testActorId2 = new ActorId("TestActor2", Guid.NewGuid().ToString());
var testActorId1 = new ActorId("TestActor1", Guid.NewGuid().ToString("N"));
var testActorId2 = new ActorId("TestActor2", Guid.NewGuid().ToString("N"));
const string Key = "sharedKey";
var value1 = JsonSerializer.SerializeToElement("value1");
@@ -243,7 +243,7 @@ public class CosmosActorStateStorageTests
using var cts = new CancellationTokenSource(s_defaultTimeout);
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
var emptyOperations = new List<ActorStateWriteOperation>();
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await storage.WriteStateAsync(testActorId, emptyOperations, "0", cancellationToken));
@@ -257,7 +257,7 @@ public class CosmosActorStateStorageTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
var readOperations = new List<ActorStateReadOperation>
{
@@ -282,7 +282,7 @@ public class CosmosActorStateStorageTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
// Create a complex object with various types
var complexObject = new
@@ -353,7 +353,7 @@ public class CosmosActorStateStorageTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key1 = "key1";
const string Key2 = "key2";
@@ -420,7 +420,7 @@ public class CosmosActorStateStorageTests
var cancellationToken = cts.Token;
await using var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
// Test keys with special characters that need sanitization
var specialKeys = new[]
@@ -74,7 +74,7 @@ public class LazyCosmosContainerTests
Assert.Equal(testContainerName, container.Id);
// Verify the container can perform basic operations
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
await using var storage = new CosmosActorStateStorage(lazyContainer);
const string Key = "testKey";
@@ -219,7 +219,7 @@ public class LazyCosmosContainerTests
{
// Act - Create storage using the internal constructor (like DI would)
await using var storage = new CosmosActorStateStorage(lazyContainer);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString());
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString("N"));
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
@@ -6,7 +6,6 @@ using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -35,43 +34,107 @@ public class AgentWorkflowBuilderTests
[Fact]
public void BuildHandoffs_InvalidArguments_Throws()
{
Assert.Throws<ArgumentNullException>("initialAgent", () => AgentWorkflowBuilder.StartHandoffWith(null!));
Assert.Throws<ArgumentNullException>("initialAgent", () => AgentWorkflowBuilder.CreateHandoffBuilderWith(null!));
var agent = new DoubleEchoAgent("agent");
var handoffs = AgentWorkflowBuilder.StartHandoffWith(agent);
var handoffs = AgentWorkflowBuilder.CreateHandoffBuilderWith(agent);
Assert.NotNull(handoffs);
Assert.Throws<ArgumentNullException>("from", () => handoffs.WithHandoff(null!, new DoubleEchoAgent("a2")));
Assert.Throws<ArgumentNullException>("to", () => handoffs.WithHandoff(new DoubleEchoAgent("a2"), (AIAgent)null!));
Assert.Throws<ArgumentNullException>("to", () => handoffs.WithHandoff(new DoubleEchoAgent("a2"), null!));
Assert.Throws<ArgumentNullException>("to", () => handoffs.WithHandoff(new DoubleEchoAgent("a2"), [null!]));
Assert.Throws<ArgumentNullException>("from", () => handoffs.WithHandoffs(null!, new DoubleEchoAgent("a2")));
Assert.Throws<ArgumentNullException>("from", () => handoffs.WithHandoffs([null!], new DoubleEchoAgent("a2")));
Assert.Throws<ArgumentNullException>("to", () => handoffs.WithHandoffs(new DoubleEchoAgent("a2"), null!));
Assert.Throws<ArgumentNullException>("to", () => handoffs.WithHandoffs(new DoubleEchoAgent("a2"), [null!]));
var noDescriptionAgent = new ChatClientAgent(new MockChatClient(delegate { return new(); }));
Assert.Throws<ArgumentException>("to", () => handoffs.WithHandoff(agent, noDescriptionAgent));
}
[Fact]
public async Task BuildSequential_AgentsRunInOrderAsync()
public void BuildGroupChat_InvalidArguments_Throws()
{
Assert.Throws<ArgumentNullException>("managerFactory", () => AgentWorkflowBuilder.CreateGroupChatBuilderWith(null!));
var groupChat = AgentWorkflowBuilder.CreateGroupChatBuilderWith(_ => new AgentWorkflowBuilder.RoundRobinGroupChatManager([new DoubleEchoAgent("a1")]));
Assert.NotNull(groupChat);
Assert.Throws<ArgumentNullException>("agents", () => groupChat.AddParticipants(null!));
Assert.Throws<ArgumentNullException>("agents", () => groupChat.AddParticipants([null!]));
Assert.Throws<ArgumentNullException>("agents", () => groupChat.AddParticipants(new DoubleEchoAgent("a1"), null!));
Assert.Throws<ArgumentNullException>("agents", () => new AgentWorkflowBuilder.RoundRobinGroupChatManager(null!));
}
[Fact]
public void GroupChatManager_MaximumIterationCount_Invalid_Throws()
{
var manager = new AgentWorkflowBuilder.RoundRobinGroupChatManager([new DoubleEchoAgent("a1")]);
const int DefaultMaxIterations = 40;
Assert.Equal(DefaultMaxIterations, manager.MaximumIterationCount);
Assert.Throws<ArgumentOutOfRangeException>("value", () => manager.MaximumIterationCount = 0);
Assert.Throws<ArgumentOutOfRangeException>("value", () => manager.MaximumIterationCount = -1);
Assert.Equal(DefaultMaxIterations, manager.MaximumIterationCount);
manager.MaximumIterationCount = 30;
Assert.Equal(30, manager.MaximumIterationCount);
manager.MaximumIterationCount = 1;
Assert.Equal(1, manager.MaximumIterationCount);
manager.MaximumIterationCount = int.MaxValue;
Assert.Equal(int.MaxValue, manager.MaximumIterationCount);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
public async Task BuildSequential_AgentsRunInOrderAsync(int numAgents)
{
var workflow = AgentWorkflowBuilder.BuildSequential(
new DoubleEchoAgent("agent1"),
new DoubleEchoAgent("agent2"),
new DoubleEchoAgent("agent3"));
from i in Enumerable.Range(1, numAgents)
select new DoubleEchoAgent($"agent{i}"));
for (int iter = 0; iter < 3; iter++)
{
(string updateText, List<ChatMessage>? result) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "abc")]);
const string Expected = "agent1abcabcagent2agent1abcabcagent1abcabcagent3agent2agent1abcabcagent1abcabcagent2agent1abcabcagent1abcabc";
Assert.Equal(Expected, updateText);
const string UserInput = "abc";
(string updateText, List<ChatMessage>? result) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]);
Assert.NotNull(result);
Assert.NotNull(Assert.Single(result));
Assert.Equal(numAgents + 1, result.Count);
Assert.Equal(ChatRole.User, result[0].Role);
Assert.Null(result[0].AuthorName);
Assert.Equal(UserInput, result[0].Text);
string[] texts = new string[numAgents + 1];
texts[0] = UserInput;
string expectedTotal = string.Empty;
for (int i = 1; i < numAgents + 1; i++)
{
string id = $"agent{((i - 1) % numAgents) + 1}";
texts[i] = $"{id}{Double(string.Concat(texts.Take(i)))}";
Assert.Equal(ChatRole.Assistant, result[i].Role);
Assert.Equal(id, result[i].AuthorName);
Assert.Equal(texts[i], result[i].Text);
expectedTotal += texts[i];
}
Assert.Equal(expectedTotal, updateText);
Assert.Equal(UserInput + expectedTotal, string.Concat(result));
static string Double(string s) => s + s;
}
}
private class DoubleEchoAgent(string name) : AIAgent
{
public override string Name => name;
public override AgentThread GetNewThread()
=> new DoubleEchoAgentThread();
@@ -85,14 +148,13 @@ public class AgentWorkflowBuilderTests
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
string id = Guid.NewGuid().ToString("N");
var contents = messages.SelectMany(m => m.Contents).ToList();
await Task.Yield();
yield return new AgentRunResponseUpdate(ChatRole.Assistant, name) { MessageId = id };
yield return new AgentRunResponseUpdate(ChatRole.Assistant, contents) { MessageId = id };
yield return new AgentRunResponseUpdate(ChatRole.Assistant, contents) { MessageId = id };
var contents = messages.SelectMany(m => m.Contents).ToList();
string id = Guid.NewGuid().ToString("N");
yield return new AgentRunResponseUpdate(ChatRole.Assistant, this.Name) { AuthorName = this.Name, MessageId = id };
yield return new AgentRunResponseUpdate(ChatRole.Assistant, contents) { AuthorName = this.Name, MessageId = id };
yield return new AgentRunResponseUpdate(ChatRole.Assistant, contents) { AuthorName = this.Name, MessageId = id };
}
}
@@ -116,13 +178,13 @@ public class AgentWorkflowBuilderTests
remaining.Value = 2;
(string updateText, List<ChatMessage>? result) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "abc")]);
Assert.Single(Regex.Matches(updateText, "agent1"));
Assert.Single(Regex.Matches(updateText, "agent2"));
Assert.NotEmpty(updateText);
Assert.NotNull(result);
// TODO: https://github.com/microsoft/agent-framework/issues/784
// These asserts are flaky until we guarantee message delivery order.
//Assert.Single(Regex.Matches(updateText, "agent1"));
//Assert.Single(Regex.Matches(updateText, "agent2"));
//Assert.Equal(4, Regex.Matches(updateText, "abc").Count);
//Assert.Equal(2, result.Count);
}
@@ -136,18 +198,11 @@ public class AgentWorkflowBuilderTests
ChatMessage message = Assert.Single(messages);
Assert.Equal("abc", Assert.IsType<TextContent>(Assert.Single(message.Contents)).Text);
string? endFunctionName = options?.Tools?.FirstOrDefault(t => t.Name.StartsWith("end", StringComparison.Ordinal))?.Name;
Assert.NotNull(endFunctionName);
return new(new ChatMessage(ChatRole.Assistant,
[
new TextContent("Hello from agent1"),
new FunctionCallContent("call12345", endFunctionName),
]));
return new(new ChatMessage(ChatRole.Assistant, "Hello from agent1"));
}));
var workflow =
AgentWorkflowBuilder.StartHandoffWith(initialAgent)
AgentWorkflowBuilder.CreateHandoffBuilderWith(initialAgent)
.WithHandoff(initialAgent, new ChatClientAgent(new MockChatClient(delegate
{
Assert.Fail("Should never be invoked.");
@@ -184,19 +239,12 @@ public class AgentWorkflowBuilderTests
}), name: "initialAgent");
var nextAgent = new ChatClientAgent(new MockChatClient((messages, options) =>
{
string? endFunctionName = options?.Tools?.FirstOrDefault(t => t.Name.StartsWith("end", StringComparison.Ordinal))?.Name;
Assert.NotNull(endFunctionName);
return new(new ChatMessage(ChatRole.Assistant,
[
new TextContent("Hello from agent2"),
new FunctionCallContent("call2", endFunctionName),
]));
}), name: "nextAgent", description: "The second agent");
new(new ChatMessage(ChatRole.Assistant, "Hello from agent2"))),
name: "nextAgent",
description: "The second agent");
var workflow =
AgentWorkflowBuilder.StartHandoffWith(initialAgent)
AgentWorkflowBuilder.CreateHandoffBuilderWith(initialAgent)
.WithHandoff(initialAgent, nextAgent)
.Build();
@@ -247,19 +295,12 @@ public class AgentWorkflowBuilderTests
}), name: "secondAgent", description: "The second agent");
var thirdAgent = new ChatClientAgent(new MockChatClient((messages, options) =>
{
string? endFunctionName = options?.Tools?.FirstOrDefault(t => t.Name.StartsWith("end", StringComparison.Ordinal))?.Name;
Assert.NotNull(endFunctionName);
return new(new ChatMessage(ChatRole.Assistant,
[
new TextContent("Hello from agent3"),
new FunctionCallContent("call3", endFunctionName),
]));
}), name: "thirdAgent", description: "The third / final agent");
new(new ChatMessage(ChatRole.Assistant, "Hello from agent3"))),
name: "thirdAgent",
description: "The third / final agent");
var workflow =
AgentWorkflowBuilder.StartHandoffWith(initialAgent)
AgentWorkflowBuilder.CreateHandoffBuilderWith(initialAgent)
.WithHandoff(initialAgent, secondAgent)
.WithHandoff(secondAgent, thirdAgent)
.Build();
@@ -294,6 +335,52 @@ public class AgentWorkflowBuilderTests
Assert.Contains("thirdAgent", result[5].AuthorName);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
public async Task BuildGroupChat_AgentsRunInOrderAsync(int maxIterations)
{
const int NumAgents = 3;
var workflow = AgentWorkflowBuilder.CreateGroupChatBuilderWith(agents => new AgentWorkflowBuilder.RoundRobinGroupChatManager(agents) { MaximumIterationCount = maxIterations })
.AddParticipants(new DoubleEchoAgent("agent1"), new DoubleEchoAgent("agent2"))
.AddParticipants(new DoubleEchoAgent("agent3"))
.Build();
for (int iter = 0; iter < 3; iter++)
{
const string UserInput = "abc";
(string updateText, List<ChatMessage>? result) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]);
Assert.NotNull(result);
Assert.Equal(maxIterations + 1, result.Count);
Assert.Equal(ChatRole.User, result[0].Role);
Assert.Null(result[0].AuthorName);
Assert.Equal(UserInput, result[0].Text);
string[] texts = new string[maxIterations + 1];
texts[0] = UserInput;
string expectedTotal = string.Empty;
for (int i = 1; i < maxIterations + 1; i++)
{
string id = $"agent{((i - 1) % NumAgents) + 1}";
texts[i] = $"{id}{Double(string.Concat(texts.Take(i)))}";
Assert.Equal(ChatRole.Assistant, result[i].Role);
Assert.Equal(id, result[i].AuthorName);
Assert.Equal(texts[i], result[i].Text);
expectedTotal += texts[i];
}
Assert.Equal(expectedTotal, updateText);
Assert.Equal(UserInput + expectedTotal, string.Concat(result));
static string Double(string s) => s + s;
}
}
private static async Task<(string UpdateText, List<ChatMessage>? Result)> RunWorkflowAsync(
Workflow<List<ChatMessage>> workflow, List<ChatMessage> input)
{
@@ -30,7 +30,7 @@ internal static class TextMessageStreamingExtensions
new()
{
Role = ChatRole.Assistant,
CreatedAt = createdAt ?? DateTimeOffset.Now,
CreatedAt = createdAt ?? DateTimeOffset.UtcNow,
MessageId = messageId ?? Guid.NewGuid().ToString("N"),
ResponseId = responseId,
AgentId = agentId,
@@ -50,7 +50,7 @@ internal static class TextMessageStreamingExtensions
new(ChatRole.Assistant, contents is List<AIContent> contentsList ? contentsList : contents.ToList())
{
AuthorName = authorName,
CreatedAt = createdAt ?? DateTimeOffset.Now,
CreatedAt = createdAt ?? DateTimeOffset.UtcNow,
MessageId = messageId ?? Guid.NewGuid().ToString("N"),
RawRepresentation = rawRepresentation,
};
@@ -77,7 +77,7 @@ internal static class TextMessageStreamingExtensions
AuthorName = authorName,
MessageId = Guid.NewGuid().ToString("N"),
RawRepresentation = text,
CreatedAt = DateTimeOffset.Now,
CreatedAt = DateTimeOffset.UtcNow,
};
}
@@ -17,16 +17,11 @@ namespace Microsoft.Agents.Workflows.Sample;
internal static class Step6EntryPoint
{
public static Workflow<List<ChatMessage>> CreateWorkflow(int maxTurns)
{
GroupChatBuilder builder =
GroupChatBuilder.Create<RoundRobinGroupChatManager, RoundRobinGroupChatManagerOptions>
(options => options.MaxTurns = maxTurns)
.AddParticipant(new HelloAgent(), shouldEmitEvents: true)
.AddParticipant(new EchoAgent(), shouldEmitEvents: true);
return builder.ReduceToWorkflow();
}
public static Workflow<List<ChatMessage>> CreateWorkflow(int maxTurns) =>
AgentWorkflowBuilder
.CreateGroupChatBuilderWith(agents => new AgentWorkflowBuilder.RoundRobinGroupChatManager(agents) { MaximumIterationCount = maxTurns })
.AddParticipants(new HelloAgent(), new EchoAgent())
.Build();
public static async ValueTask RunAsync(TextWriter writer, int maxSteps = 2)
{
@@ -53,39 +48,6 @@ internal static class Step6EntryPoint
}
}
}
private sealed class RoundRobinGroupChatManagerOptions : GroupChatManagerOptions
{
public int? MaxTurns { get; set; }
}
private sealed class RoundRobinGroupChatManager() : GroupChatManager<RoundRobinGroupChatManagerOptions>
{
public int TurnCount { get; private set; }
public int? MaxTurns { get; private set; }
protected internal override void Configure(RoundRobinGroupChatManagerOptions options)
{
base.Configure(options);
this.MaxTurns = options.MaxTurns;
}
public override int? GetNextTurnExecutor(GroupChatHistory history)
{
if (this.ParticipantIds.Length == 0)
{
throw new InvalidOperationException("No participants in the group chat.");
}
if (this.TurnCount >= this.MaxTurns)
{
return null;
}
return this.TurnCount++ % this.ParticipantIds.Length;
}
}
}
internal sealed class HelloAgent(string id = nameof(HelloAgent)) : AIAgent
@@ -175,184 +137,3 @@ internal sealed class EchoAgent(string id = nameof(EchoAgent)) : AIAgent
}
internal sealed class EchoAgentThread() : InMemoryAgentThread();
internal sealed class GroupChatHistory
{
private readonly List<ChatMessage> _messages = [];
private int _bookmark;
public void AddMessage(ChatMessage message) =>
this._messages.Add(message);
public void AddMessages(IEnumerable<ChatMessage> messages) =>
this._messages.AddRange(messages);
public void UpdateBookmark() =>
this._bookmark = this._messages.Count;
public IReadOnlyList<ChatMessage> FullHistory => this._messages.AsReadOnly();
public IEnumerable<ChatMessage> NewMessagesThisTurn => this._messages.Skip(this._bookmark);
}
internal class GroupChatManagerOptions;
internal abstract class GroupChatManager
{
public string[] ParticipantIds { get; internal init; } = [];
public abstract int? GetNextTurnExecutor(GroupChatHistory history);
}
internal abstract class GroupChatManager<TOptions> : GroupChatManager where TOptions : GroupChatManagerOptions, new()
{
protected internal virtual void Configure(TOptions options) { }
}
internal sealed class GroupChatBuilder
{
private readonly List<ExecutorIsh> _participants = [];
private readonly List<bool> _shouldEmitEvents = [];
private readonly Func<string[], GroupChatManager> _managerFactory;
private GroupChatBuilder(Func<string[], GroupChatManager> managerFactory)
{
this._managerFactory = managerFactory;
}
public static GroupChatBuilder Create<TManager>() where TManager : GroupChatManager, new() =>
new(participantIds => new TManager() { ParticipantIds = participantIds });
public static GroupChatBuilder Create<TManager, TOptions>(Action<TOptions> configure)
where TManager : GroupChatManager<TOptions>, new()
where TOptions : GroupChatManagerOptions, new()
{
TOptions options = new();
configure(options);
return new GroupChatBuilder(participantIds =>
{
TManager manager = new() { ParticipantIds = participantIds };
manager.Configure(options);
return manager;
});
}
public GroupChatBuilder AddParticipant(ExecutorIsh executor, bool shouldEmitEvents = false)
{
this._participants.Add(executor);
this._shouldEmitEvents.Add(shouldEmitEvents);
return this;
}
public GroupChatBuilder AddParticipants(params ExecutorIsh[] executors)
{
this._participants.AddRange(executors);
return this;
}
public Workflow<List<ChatMessage>> ReduceToWorkflow()
{
string[] participantIds = this._participants.Select(identified => identified.Id).ToArray();
GroupChatHost host = new(this._shouldEmitEvents.ToArray(), this._managerFactory(participantIds));
WorkflowBuilder builder = new WorkflowBuilder(host)
.AddFanOutEdge(host, targets: this._participants.ToArray());
foreach (ExecutorIsh participant in this._participants)
{
builder.AddEdge(participant, host);
}
return builder.Build<List<ChatMessage>>();
//bool IsMessageType(object? message) => message is ChatMessage || message is IEnumerable<ChatMessage>;
}
private sealed class TurnAssignedEvent(string executorId, string nextSpeakerId) : ExecutorEvent(executorId, data: nextSpeakerId);
private sealed class GroupChatHost : Executor
{
private readonly bool[] _shouldEmitEvents;
private readonly GroupChatManager _manager;
private readonly bool _autoStartConversation;
private readonly GroupChatHistory _history = new();
public GroupChatHost(bool[] shouldEmitEvents, GroupChatManager manager, bool autoStartConversation = false) : base(nameof(GroupChatHost))
{
this._shouldEmitEvents = shouldEmitEvents;
this._manager = manager ?? throw new ArgumentNullException(nameof(manager));
this._autoStartConversation = autoStartConversation;
}
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler<List<ChatMessage>>(this.HandleChatMessagesAsync)
.AddHandler<ChatMessage>(this.HandleChatMessageAsync)
.AddHandler<TurnToken>(this.AssignNextTurnAsync);
private async Task TryAutoStartConversationAsync(IWorkflowContext context)
{
if (this._autoStartConversation && this.TryEnterConversation())
{
await this.AssignNextTurnAsync(new TurnToken(emitEvents: false), context).ConfigureAwait(false);
}
}
private async ValueTask HandleChatMessagesAsync(List<ChatMessage> initialMessages, IWorkflowContext context)
{
this._history.AddMessages(initialMessages);
await context.SendMessageAsync(initialMessages).ConfigureAwait(false);
await this.TryAutoStartConversationAsync(context).ConfigureAwait(false);
}
private async ValueTask HandleChatMessageAsync(ChatMessage message, IWorkflowContext context)
{
// First, add the message to the history, then forward to all executors
this._history.AddMessage(message);
await context.SendMessageAsync(message).ConfigureAwait(false);
await this.TryAutoStartConversationAsync(context).ConfigureAwait(false);
}
private int _inConversationFlag;
/// <summary>
/// Atomically switches to "in conversation" state if not already in that state.
/// </summary>
/// <returns><see langword="true"/> if the state was changed, <see langword="false"/> otherwise.</returns>
private bool TryEnterConversation() =>
Interlocked.CompareExchange(ref this._inConversationFlag, 1, 0) == 0;
private bool _shouldHostEmitEvents;
private async ValueTask AssignNextTurnAsync(TurnToken token, IWorkflowContext context)
{
if (this.TryEnterConversation())
{
// Capture the initial turn token's EmitEvents setting
this._shouldHostEmitEvents = token.EmitEvents ?? false;
}
int? nextSpeakerIndex = this._manager.GetNextTurnExecutor(this._history);
if (nextSpeakerIndex is null)
{
await context.AddEventAsync(new WorkflowCompletedEvent())
.ConfigureAwait(false);
return;
}
string nextSpeakerId = this._manager.ParticipantIds[nextSpeakerIndex.Value];
if (this._shouldHostEmitEvents)
{
await context.AddEventAsync(new TurnAssignedEvent(this.Id, nextSpeakerId))
.ConfigureAwait(false);
}
await context.SendMessageAsync(new TurnToken(this._shouldEmitEvents[nextSpeakerIndex.Value]), nextSpeakerId)
.ConfigureAwait(false);
}
}
}
@@ -43,7 +43,7 @@ public class SpecializedExecutorSmokeTests
{
MessageId = Guid.NewGuid().ToString("N"),
RawRepresentation = text,
CreatedAt = DateTime.Now,
CreatedAt = DateTime.UtcNow,
};
}
@@ -2,8 +2,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
#pragma warning disable CA1861 // Avoid constant arrays as arguments
@@ -148,12 +146,5 @@ public class AgentThreadTests
#endregion
private sealed class TestAgentThread : AgentThread
{
protected internal override Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
=> base.MessagesReceivedAsync(newMessages, cancellationToken);
public override Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> base.SerializeAsync(jsonSerializerOptions, cancellationToken);
}
private sealed class TestAgentThread : AgentThread;
}
@@ -78,7 +78,7 @@ public class ChatMessageStoreTests
private sealed class TestChatMessageStore : ChatMessageStore
{
public override Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
=> Task.FromResult<IEnumerable<ChatMessage>>(Array.Empty<ChatMessage>());
=> Task.FromResult<IEnumerable<ChatMessage>>([]);
public override Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
@@ -300,9 +300,7 @@ public class DelegatingAIAgentTests
public new AIAgent InnerAgent => base.InnerAgent;
}
private sealed class TestAgentThread : AgentThread
{
}
private sealed class TestAgentThread : AgentThread;
#endregion
}
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
@@ -31,8 +30,7 @@ public class InMemoryAgentThreadTests
public void Constructor_WithMessageStore_SetsProperty()
{
// Arrange
var store = new InMemoryChatMessageStore();
store.Add(new ChatMessage(ChatRole.User, "Hello"));
InMemoryChatMessageStore store = [new(ChatRole.User, "Hello")];
// Act
var thread = new TestInMemoryAgentThread(store);
@@ -62,8 +60,7 @@ public class InMemoryAgentThreadTests
public async Task Constructor_WithSerializedState_SetsPropertyAsync()
{
// Arrange
var store = new InMemoryChatMessageStore();
store.Add(new ChatMessage(ChatRole.User, "TestMsg"));
InMemoryChatMessageStore store = [new(ChatRole.User, "TestMsg")];
var storeState = await store.SerializeStateAsync();
var json = JsonSerializer.SerializeToElement(new { storeState });
@@ -94,9 +91,7 @@ public class InMemoryAgentThreadTests
public async Task SerializeAsync_ReturnsCorrectJson_WhenMessagesExistAsync()
{
// Arrange
var store = new InMemoryChatMessageStore();
store.Add(new ChatMessage(ChatRole.User, "TestContent"));
var thread = new TestInMemoryAgentThread(store);
var thread = new TestInMemoryAgentThread([new(ChatRole.User, "TestContent")]);
// Act
var json = await thread.SerializeAsync();
@@ -150,11 +145,10 @@ public class InMemoryAgentThreadTests
// Sealed test subclass to expose protected members for testing
private sealed class TestInMemoryAgentThread : InMemoryAgentThread
{
public TestInMemoryAgentThread() : base() { }
public TestInMemoryAgentThread() { }
public TestInMemoryAgentThread(InMemoryChatMessageStore? store) : base(store) { }
public TestInMemoryAgentThread(IEnumerable<ChatMessage> messages) : base(messages) { }
public TestInMemoryAgentThread(JsonElement serializedThreadState) : base(serializedThreadState) { }
public InMemoryChatMessageStore GetMessageStore() => this.MessageStore;
public override Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => base.SerializeAsync(jsonSerializerOptions, cancellationToken);
}
}
@@ -2,7 +2,6 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
@@ -108,10 +107,9 @@ public class ServiceIdAgentThreadTests
// Sealed test subclass to expose protected members for testing
private sealed class TestServiceIdAgentThread : ServiceIdAgentThread
{
public TestServiceIdAgentThread() : base() { }
public TestServiceIdAgentThread() { }
public TestServiceIdAgentThread(string serviceThreadId) : base(serviceThreadId) { }
public TestServiceIdAgentThread(JsonElement serializedThreadState) : base(serializedThreadState) { }
public string? GetServiceThreadId() => this.ServiceThreadId;
public override Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => base.SerializeAsync(jsonSerializerOptions, cancellationToken);
}
}
@@ -104,8 +104,10 @@ public class AgentActorTests
var threadJson = JsonSerializer.SerializeToElement(new { conversationId = "expected-thread-id" });
var mockThread = new Mock<AgentThread>();
var testAgent = new TestAgent();
testAgent.ThreadForCreate = mockThread.Object;
TestAgent testAgent = new()
{
ThreadForCreate = mockThread.Object
};
var mockContext = new Mock<IActorRuntimeContext>();
var actorId = new ActorId("TestAgent", "test-instance");
@@ -239,8 +239,7 @@ public class ChatClientAgentThreadTests
public async Task VerifyThreadSerializationWithMessagesAsync()
{
// Arrange
var store = new InMemoryChatMessageStore();
store.Add(new ChatMessage(ChatRole.User, "TestContent") { AuthorName = "TestAuthor" });
InMemoryChatMessageStore store = [new(ChatRole.User, "TestContent") { AuthorName = "TestAuthor" }];
var thread = new ChatClientAgentThread { MessageStore = store };
// Act
@@ -273,13 +272,15 @@ public class ChatClientAgentThreadTests
{
// Arrange
Mock<AIContextProvider> mockProvider = new();
var providerStateElement = JsonSerializer.SerializeToElement(new[] { "CP1" }, TestJsonSerializerContext.Default.StringArray);
var providerStateElement = JsonSerializer.SerializeToElement(["CP1"], TestJsonSerializerContext.Default.StringArray);
mockProvider
.Setup(m => m.SerializeAsync(It.IsAny<JsonSerializerOptions?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(providerStateElement);
var thread = new ChatClientAgentThread();
thread.AIContextProvider = mockProvider.Object;
var thread = new ChatClientAgentThread
{
AIContextProvider = mockProvider.Object
};
// Act
var json = await thread.SerializeAsync();
@@ -23,7 +23,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_ExpectedTelemetryData_CollectedAsync(bool withError)
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -91,7 +91,7 @@ public class OpenTelemetryAgentTests
public async Task RunStreamingAsync_ExpectedTelemetryData_CollectedAsync(bool withError)
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -165,7 +165,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithChatClientAgent_IncludesInstructionsAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -204,7 +204,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithChatClientAgent_WithMetadata_UsesProviderNameAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -248,7 +248,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithNonChatClientAgent_UsesDefaultSystemAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -279,7 +279,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithChatClientAgent_WithDifferentProviders_UsesCorrectSystemAsync(string providerName)
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -321,7 +321,7 @@ public class OpenTelemetryAgentTests
public async Task RunStreamingAsync_WithChatClientAgent_WithMetadata_UsesProviderNameAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -372,7 +372,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithThreadId_IncludesThreadIdAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -471,7 +471,7 @@ public class OpenTelemetryAgentTests
public async Task WithOpenTelemetry_EnableSensitiveDataParameter_SetsPropertyCorrectlyAsync(bool enableSensitiveData)
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -592,7 +592,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithILogger_LogsEventsCorrectlyAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var mockLogger = new Mock<ILogger>();
// Setup the logger to return true for IsEnabled to ensure logging occurs
@@ -658,7 +658,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithOpenTelemetryChatClientAgent_DoesNotDuplicateLogsAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -728,7 +728,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithRegularChatClientAgent_LogsCorrectlyAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -793,7 +793,7 @@ public class OpenTelemetryAgentTests
public void Constructor_WithOpenTelemetryChatClient_InheritsEnableSensitiveDataSetting()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var mockChatClient = new Mock<IChatClient>();
// Setup ChatClientMetadata
@@ -1097,7 +1097,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithChatClientAgent_ProviderNameReflectedInTelemetryAsync(string providerName)
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1146,7 +1146,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithChatClientAgent_NullMetadata_DefaultsToMEAIAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1195,7 +1195,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithCustomAgent_CustomMetadata_ReflectedInTelemetryAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1242,7 +1242,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithCustomAgent_NoMetadata_DefaultsToMEAIAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1288,7 +1288,7 @@ public class OpenTelemetryAgentTests
public async Task RunStreamingAsync_WithChatClientAgent_ProviderNameReflectedInTelemetryAsync(string providerName)
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1346,7 +1346,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_MultipleCallsWithSameAgent_ConsistentProviderNameAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1567,7 +1567,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithNullResponseId_HandlesGracefullyAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1608,7 +1608,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithEmptyAgentName_UsesOperationNameOnlyAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1642,7 +1642,7 @@ public class OpenTelemetryAgentTests
public async Task RunStreamingAsync_WithPartialUpdates_CombinesCorrectlyAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -1733,7 +1733,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithMetricsEnabled_RecordsMetricsAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
var exportedMetrics = new List<Metric>();
@@ -1777,7 +1777,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithMetricsEnabledAndError_RecordsErrorMetricsAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
var exportedMetrics = new List<Metric>();
@@ -1817,7 +1817,7 @@ public class OpenTelemetryAgentTests
public async Task RunStreamingAsync_WithMetricsEnabled_RecordsMetricsAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
var exportedMetrics = new List<Metric>();
@@ -1866,7 +1866,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithNullUsage_SkipsTokenMetricsAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var exportedMetrics = new List<Metric>();
using var meterProvider = OpenTelemetry.Sdk.CreateMeterProviderBuilder()
@@ -1914,7 +1914,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithMetricsDisabled_SkipsMetricRecordingAsync()
{
// Arrange - No meter provider, so metrics are disabled
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
@@ -1945,7 +1945,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithPartialTokenUsage_RecordsAvailableTokensAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var exportedMetrics = new List<Metric>();
using var meterProvider = OpenTelemetry.Sdk.CreateMeterProviderBuilder()
@@ -1993,7 +1993,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithNullDescription_SkipsDescriptionAttributeAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2034,7 +2034,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithAssistantMessage_LogsAssistantEventAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2084,7 +2084,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithToolMessage_LogsToolEventAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2134,7 +2134,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithToolMessageAndSensitiveData_LogsToolEventWithContentAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2197,7 +2197,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithFunctionCallAndSensitiveDataEnabled_LogsWithSensitiveContentAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2285,7 +2285,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithToolMessageAndSensitiveDataDisabled_LogsToolEventWithoutContentAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2350,7 +2350,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithFunctionCallAndSensitiveDataDisabled_LogsWithoutSensitiveContentAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2431,7 +2431,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_LoggerNotEnabled_DoesNotLogChatResponseAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2482,7 +2482,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithResponseMessages_LogsChoiceEventsAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2546,7 +2546,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithSingleResponseMessage_LogsChoiceEventAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
@@ -2602,7 +2602,7 @@ public class OpenTelemetryAgentTests
public void JsonSerializerOptions_GetterReturnsSetValue()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var mockLogger = new Mock<ILogger>();
var mockAgent = new Mock<AIAgent>();
@@ -2617,7 +2617,7 @@ public class OpenTelemetryAgentTests
public void JsonSerializerOptions_SetterUpdatesValue()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var mockLogger = new Mock<ILogger>();
var mockAgent = new Mock<AIAgent>();
@@ -2640,7 +2640,7 @@ public class OpenTelemetryAgentTests
public void JsonSerializerOptions_SetterThrowsOnNull()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var mockLogger = new Mock<ILogger>();
var mockAgent = new Mock<AIAgent>();
@@ -2654,7 +2654,7 @@ public class OpenTelemetryAgentTests
public async Task RunAsync_WithCustomJsonSerializerOptions_UsesCustomOptionsForSerializationAsync()
{
// Arrange
var sourceName = Guid.NewGuid().ToString();
var sourceName = Guid.NewGuid().ToString("N");
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)