diff --git a/dotnet/samples/AgentSyntaxExamples/Example01_Agent.cs b/dotnet/samples/AgentSyntaxExamples/Example01_Agent.cs
index 4c113b9a1682..17370ddc0265 100644
--- a/dotnet/samples/AgentSyntaxExamples/Example01_Agent.cs
+++ b/dotnet/samples/AgentSyntaxExamples/Example01_Agent.cs
@@ -1,6 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Collections.Generic;
-using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
@@ -32,7 +30,7 @@ public async Task RunAsync()
};
// Create a chat for agent interaction. For more, see: Example03_Chat.
- var chat = new TestChat();
+ AgentGroupChat chat = new();
// Respond to user input
await InvokeAgentAsync("Fortune favors the bold.");
@@ -52,18 +50,4 @@ async Task InvokeAgentAsync(string input)
}
}
}
-
- ///
- /// A simple chat for the agent example.
- ///
- ///
- /// For further exploration of , see: Example03_Chat.
- ///
- private sealed class TestChat : AgentChat
- {
- public IAsyncEnumerable InvokeAsync(
- Agent agent,
- CancellationToken cancellationToken = default) =>
- base.InvokeAgentAsync(agent, cancellationToken);
- }
}
diff --git a/dotnet/samples/AgentSyntaxExamples/Example02_Plugins.cs b/dotnet/samples/AgentSyntaxExamples/Example02_Plugins.cs
index 5f81d41b6d7f..6e4910245350 100644
--- a/dotnet/samples/AgentSyntaxExamples/Example02_Plugins.cs
+++ b/dotnet/samples/AgentSyntaxExamples/Example02_Plugins.cs
@@ -1,6 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Collections.Generic;
-using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
@@ -39,7 +37,7 @@ public async Task RunAsync()
agent.Kernel.Plugins.Add(plugin);
// Create a chat for agent interaction. For more, see: Example03_Chat.
- var chat = new TestChat();
+ AgentGroupChat chat = new();
// Respond to user input, invoking functions where appropriate.
await InvokeAgentAsync("Hello");
@@ -59,19 +57,4 @@ async Task InvokeAgentAsync(string input)
}
}
}
-
- ///
- ///
- /// A simple chat for the agent example.
- ///
- ///
- /// For further exploration of , see: Example03_Chat.
- ///
- private sealed class TestChat : AgentChat
- {
- public IAsyncEnumerable InvokeAsync(
- Agent agent,
- CancellationToken cancellationToken = default) =>
- base.InvokeAgentAsync(agent, cancellationToken);
- }
}
diff --git a/dotnet/samples/AgentSyntaxExamples/Example03_Chat.cs b/dotnet/samples/AgentSyntaxExamples/Example03_Chat.cs
new file mode 100644
index 000000000000..fedc422ce503
--- /dev/null
+++ b/dotnet/samples/AgentSyntaxExamples/Example03_Chat.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.Chat;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Examples;
+
+///
+/// Demonstrate creation of with
+/// that inform how chat proceeds with regards to: Agent selection, chat continuation, and maximum
+/// number of agent interactions.
+///
+public class Example03_Chat(ITestOutputHelper output) : BaseTest(output)
+{
+ private const string ReviewerName = "ArtDirector";
+ private const string ReviewerInstructions =
+ """
+ You are an art director who has opinions about copywriting born of a love for David Ogilvy.
+ The goal is to determine is the given copy is acceptable to print.
+ If so, state that it is approved.
+ If not, provide insight on how to refine suggested copy without example.
+ """;
+
+ private const string CopyWriterName = "Writer";
+ private const string CopyWriterInstructions =
+ """
+ You are a copywriter with ten years of experience and are known for brevity and a dry humor.
+ You're laser focused on the goal at hand. Don't waste time with chit chat.
+ The goal is to refine and decide on the single best copy as an expert in the field.
+ Consider suggestions when refining an idea.
+ """;
+
+ [Fact]
+ public async Task RunAsync()
+ {
+ // Define the agents
+ ChatCompletionAgent agentReviewer =
+ new()
+ {
+ Instructions = ReviewerInstructions,
+ Name = ReviewerName,
+ Kernel = this.CreateKernelWithChatCompletion(),
+ };
+
+ ChatCompletionAgent agentWriter =
+ new()
+ {
+ Instructions = CopyWriterInstructions,
+ Name = CopyWriterName,
+ Kernel = this.CreateKernelWithChatCompletion(),
+ };
+
+ // Create a chat for agent interaction.
+ AgentGroupChat chat =
+ new(agentWriter, agentReviewer)
+ {
+ ExecutionSettings =
+ new()
+ {
+ // Here a TerminationStrategy subclass is used that will terminate when
+ // an assistant message contains the term "approve".
+ TerminationStrategy =
+ new ApprovalTerminationStrategy()
+ {
+ // Only the art-director may approve.
+ Agents = [agentReviewer],
+ }
+ }
+ };
+
+ // Invoke chat and display messages.
+ string input = "concept: maps made out of egg cartons.";
+ chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
+ this.WriteLine($"# {AuthorRole.User}: '{input}'");
+
+ await foreach (var content in chat.InvokeAsync())
+ {
+ this.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ }
+
+ this.WriteLine($"# IS COMPLETE: {chat.IsComplete}");
+ }
+
+ private sealed class ApprovalTerminationStrategy : TerminationStrategy
+ {
+ // Terminate when the final message contains the term "approve"
+ protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken)
+ => Task.FromResult(history[history.Count - 1].Content?.Contains("approve", StringComparison.OrdinalIgnoreCase) ?? false);
+ }
+}
diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs
index 6f436029e8b4..e3004ce00ef0 100644
--- a/dotnet/src/Agents/Abstractions/AgentChat.cs
+++ b/dotnet/src/Agents/Abstractions/AgentChat.cs
@@ -19,10 +19,14 @@ public abstract class AgentChat
private readonly BroadcastQueue _broadcastQueue;
private readonly Dictionary _agentChannels;
private readonly Dictionary _channelMap;
- private readonly ChatHistory _history;
private int _isActive;
+ ///
+ /// Exposes the internal history to subclasses.
+ ///
+ protected ChatHistory History { get; }
+
///
/// Retrieve the message history, either the primary history or
/// an agent specific version.
@@ -34,7 +38,7 @@ public IAsyncEnumerable GetChatMessagesAsync(Agent? agent =
{
if (agent == null)
{
- return this._history.ToDescendingAsync();
+ return this.History.ToDescendingAsync();
}
var channelKey = this.GetAgentHash(agent);
@@ -80,7 +84,7 @@ public void AddChatMessages(IReadOnlyList messages)
}
// Append to chat history
- this._history.AddRange(messages);
+ this.History.AddRange(messages);
// Broadcast message to other channels (in parallel)
var channelRefs = this._agentChannels.Select(kvp => new ChannelReference(kvp.Value, kvp.Key));
@@ -114,7 +118,7 @@ protected async IAsyncEnumerable InvokeAgentAsync(
await foreach (var message in channel.InvokeAsync(agent, cancellationToken).ConfigureAwait(false))
{
// Add to primary history
- this._history.Add(message);
+ this.History.Add(message);
messages.Add(message);
// Yield message to caller
@@ -146,9 +150,9 @@ private async Task GetChannelAsync(Agent agent, CancellationToken
{
channel = await agent.CreateChannelAsync(cancellationToken).ConfigureAwait(false);
- if (this._history.Count > 0)
+ if (this.History.Count > 0)
{
- await channel.ReceiveAsync(this._history, cancellationToken).ConfigureAwait(false);
+ await channel.ReceiveAsync(this.History, cancellationToken).ConfigureAwait(false);
}
this._agentChannels.Add(channelKey, channel);
@@ -179,6 +183,6 @@ protected AgentChat()
this._agentChannels = [];
this._broadcastQueue = new();
this._channelMap = [];
- this._history = [];
+ this.History = [];
}
}
diff --git a/dotnet/src/Agents/Core/AgentGroupChat.cs b/dotnet/src/Agents/Core/AgentGroupChat.cs
new file mode 100644
index 000000000000..70e343834642
--- /dev/null
+++ b/dotnet/src/Agents/Core/AgentGroupChat.cs
@@ -0,0 +1,148 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.Agents.Chat;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace Microsoft.SemanticKernel.Agents;
+
+///
+/// A an that supports multi-turn interactions.
+///
+public sealed class AgentGroupChat : AgentChat
+{
+ private readonly HashSet _agentIds; // Efficient existence test
+ private readonly List _agents; // Maintain order
+
+ ///
+ /// Indicates if completion criteria has been met. If set, no further
+ /// agent interactions will occur. Clear to enable more agent interactions.
+ ///
+ public bool IsComplete { get; set; }
+
+ ///
+ /// Settings for defining chat behavior.
+ ///
+ public AgentGroupChatSettings ExecutionSettings { get; set; } = new AgentGroupChatSettings();
+
+ ///
+ /// The agents participating in the chat.
+ ///
+ public IReadOnlyList Agents => this._agents.AsReadOnly();
+
+ ///
+ /// Add a to the chat.
+ ///
+ /// The to add.
+ public void AddAgent(Agent agent)
+ {
+ if (this._agentIds.Add(agent.Id))
+ {
+ this._agents.Add(agent);
+ }
+ }
+
+ ///
+ /// Process a series of interactions between the that have joined this .
+ /// The interactions will proceed according to the and the
+ /// defined via .
+ /// In the absence of an , this method will not invoke any agents.
+ /// Any agent may be explicitly selected by calling .
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// Asynchronous enumeration of messages.
+ public async IAsyncEnumerable InvokeAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ if (this.IsComplete)
+ {
+ // Throw exception if chat is completed and automatic-reset is not enabled.
+ if (!this.ExecutionSettings.TerminationStrategy.AutomaticReset)
+ {
+ throw new KernelException("Agent Failure - Chat has completed.");
+ }
+
+ this.IsComplete = false;
+ }
+
+ for (int index = 0; index < this.ExecutionSettings.TerminationStrategy.MaximumIterations; index++)
+ {
+ // Identify next agent using strategy
+ Agent agent = await this.ExecutionSettings.SelectionStrategy.NextAsync(this.Agents, this.History, cancellationToken).ConfigureAwait(false);
+
+ // Invoke agent and process messages along with termination
+ await foreach (var message in base.InvokeAgentAsync(agent, cancellationToken).ConfigureAwait(false))
+ {
+ if (message.Role == AuthorRole.Assistant)
+ {
+ var task = this.ExecutionSettings.TerminationStrategy.ShouldTerminateAsync(agent, this.History, cancellationToken);
+ this.IsComplete = await task.ConfigureAwait(false);
+ }
+
+ yield return message;
+ }
+
+ if (this.IsComplete)
+ {
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Process a single interaction between a given an a .
+ ///
+ /// The agent actively interacting with the chat.
+ /// The to monitor for cancellation requests. The default is .
+ /// Asynchronous enumeration of messages.
+ ///
+ /// Specified agent joins the chat.
+ /// >
+ public IAsyncEnumerable InvokeAsync(
+ Agent agent,
+ CancellationToken cancellationToken = default) =>
+ this.InvokeAsync(agent, isJoining: true, cancellationToken);
+
+ ///
+ /// Process a single interaction between a given an a irregardless of
+ /// the defined via . Likewise, this does
+ /// not regard as it only takes a single turn for the specified agent.
+ ///
+ /// The agent actively interacting with the chat.
+ /// Optional flag to control if agent is joining the chat.
+ /// The to monitor for cancellation requests. The default is .
+ /// Asynchronous enumeration of messages.
+ public async IAsyncEnumerable InvokeAsync(
+ Agent agent,
+ bool isJoining,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ if (isJoining)
+ {
+ this.AddAgent(agent);
+ }
+
+ await foreach (var message in base.InvokeAgentAsync(agent, cancellationToken).ConfigureAwait(false))
+ {
+ if (message.Role == AuthorRole.Assistant)
+ {
+ var task = this.ExecutionSettings.TerminationStrategy.ShouldTerminateAsync(agent, this.History, cancellationToken);
+ this.IsComplete = await task.ConfigureAwait(false);
+ }
+
+ yield return message;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The agents initially participating in the chat.
+ public AgentGroupChat(params Agent[] agents)
+ {
+ this._agents = new(agents);
+ this._agentIds = new(this._agents.Select(a => a.Id));
+ }
+}
diff --git a/dotnet/src/Agents/Core/Chat/AgentGroupChatSettings.cs b/dotnet/src/Agents/Core/Chat/AgentGroupChatSettings.cs
new file mode 100644
index 000000000000..f7b2d87fb7e8
--- /dev/null
+++ b/dotnet/src/Agents/Core/Chat/AgentGroupChatSettings.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Agents.Chat;
+
+///
+/// Settings that affect behavior of .
+///
+///
+/// Default behavior result in no agent selection.
+///
+public class AgentGroupChatSettings
+{
+ ///
+ /// Strategy for selecting the next agent. Dfeault strategy limited to a single iteration and no termination criteria.
+ ///
+ ///
+ /// See .
+ ///
+ public TerminationStrategy TerminationStrategy { get; init; } = new DefaultTerminationStrategy();
+
+ ///
+ /// Strategy for selecting the next agent. Defaults to .
+ ///
+ ///
+ /// See .
+ ///
+ public SelectionStrategy SelectionStrategy { get; init; } = new SequentialSelectionStrategy();
+
+ ///
+ /// The termination strategy attached to the default state of .
+ /// This strategy will execute without signaling termination. Execution of will only be
+ /// bound by .
+ ///
+ internal sealed class DefaultTerminationStrategy : TerminationStrategy
+ {
+ ///
+ protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(false);
+ }
+
+ public DefaultTerminationStrategy()
+ {
+ this.MaximumIterations = 1;
+ }
+ }
+}
diff --git a/dotnet/src/Agents/Core/Chat/AggregatorTerminationStrategy.cs b/dotnet/src/Agents/Core/Chat/AggregatorTerminationStrategy.cs
new file mode 100644
index 000000000000..9fb3c9a47f86
--- /dev/null
+++ b/dotnet/src/Agents/Core/Chat/AggregatorTerminationStrategy.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Agents.Chat;
+
+///
+/// Defines aggregation behavior for
+///
+public enum AggregateTerminationCondition
+{
+ ///
+ /// All aggregated strategies must agree on termination.
+ ///
+ All,
+
+ ///
+ /// Any single aggregated strategy will terminate.
+ ///
+ Any,
+}
+
+///
+/// Aggregate a set of objects.
+///
+/// Set of strategies upon which to aggregate.
+public sealed class AggregatorTerminationStrategy(params TerminationStrategy[] strategies) : TerminationStrategy
+{
+ private readonly TerminationStrategy[] _strategies = strategies;
+
+ ///
+ /// Logical operation for aggregation: All or Any (and/or). Default: All.
+ ///
+ public AggregateTerminationCondition Condition { get; init; } = AggregateTerminationCondition.All;
+
+ ///
+ protected override async Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken = default)
+ {
+ var strategyExecution = this._strategies.Select(s => s.ShouldTerminateAsync(agent, history, cancellationToken));
+
+ var results = await Task.WhenAll(strategyExecution).ConfigureAwait(false);
+ bool shouldTerminate =
+ this.Condition == AggregateTerminationCondition.All ?
+ results.All(r => r) :
+ results.Any(r => r);
+
+ return shouldTerminate;
+ }
+}
diff --git a/dotnet/src/Agents/Core/Chat/RegExTerminationStrategy.cs b/dotnet/src/Agents/Core/Chat/RegExTerminationStrategy.cs
new file mode 100644
index 000000000000..3164dd4760cc
--- /dev/null
+++ b/dotnet/src/Agents/Core/Chat/RegExTerminationStrategy.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Agents.Chat;
+
+///
+/// Signals termination when the most recent message matches against the defined regular expressions
+/// for the specified agent (if provided).
+///
+public sealed class RegExTerminationStrategy : TerminationStrategy
+{
+ private readonly string[] _expressions;
+
+ ///
+ protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken = default)
+ {
+ // Most recent message
+ var message = history[history.Count - 1];
+
+ // Evaluate expressions for match
+ foreach (var expression in this._expressions)
+ {
+ if (Regex.IsMatch(message.Content, expression))
+ {
+ return Task.FromResult(true);
+ }
+ }
+
+ return Task.FromResult(false);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A list of regular expressions, that if
+ public RegExTerminationStrategy(params string[] expressions)
+ {
+ this._expressions = expressions;
+ }
+}
diff --git a/dotnet/src/Agents/Core/Chat/SelectionStrategy.cs b/dotnet/src/Agents/Core/Chat/SelectionStrategy.cs
new file mode 100644
index 000000000000..ed43df98c4b8
--- /dev/null
+++ b/dotnet/src/Agents/Core/Chat/SelectionStrategy.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Agents.Chat;
+
+///
+/// Base strategy class for selecting the next agent for a .
+///
+public abstract class SelectionStrategy
+{
+ ///
+ /// Determine which agent goes next.
+ ///
+ /// The agents participating in chat.
+ /// The chat history.
+ /// The to monitor for cancellation requests. The default is .
+ /// The agent who shall take the next turn.
+ public abstract Task NextAsync(IReadOnlyList agents, IReadOnlyList history, CancellationToken cancellationToken = default);
+}
diff --git a/dotnet/src/Agents/Core/Chat/SequentialSelectionStrategy.cs b/dotnet/src/Agents/Core/Chat/SequentialSelectionStrategy.cs
new file mode 100644
index 000000000000..0532ed90c6f1
--- /dev/null
+++ b/dotnet/src/Agents/Core/Chat/SequentialSelectionStrategy.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Agents.Chat;
+
+///
+/// Round-robin turn-taking strategy. Agent order is based on the order
+/// in which they joined .
+///
+public sealed class SequentialSelectionStrategy : SelectionStrategy
+{
+ private int _index = 0;
+
+ ///
+ /// Reset selection to initial/first agent. Agent order is based on the order
+ /// in which they joined .
+ ///
+ public void Reset() => this._index = 0;
+
+ ///
+ public override Task NextAsync(IReadOnlyList agents, IReadOnlyList history, CancellationToken cancellationToken = default)
+ {
+ if (agents.Count == 0)
+ {
+ throw new KernelException("Agent Failure - No agents present to select.");
+ }
+
+ // Set of agents array may not align with previous execution, constrain index to valid range.
+ if (this._index > agents.Count - 1)
+ {
+ this._index = 0;
+ }
+
+ var agent = agents[this._index];
+
+ this._index = (this._index + 1) % agents.Count;
+
+ return Task.FromResult(agent);
+ }
+}
diff --git a/dotnet/src/Agents/Core/Chat/TerminationStrategy.cs b/dotnet/src/Agents/Core/Chat/TerminationStrategy.cs
new file mode 100644
index 000000000000..7e86fbe063a9
--- /dev/null
+++ b/dotnet/src/Agents/Core/Chat/TerminationStrategy.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SemanticKernel.Agents.Chat;
+
+///
+/// Base strategy class for defining termination criteria for a .
+///
+public abstract class TerminationStrategy
+{
+ ///
+ /// Restrict number of turns to a reasonable number (99).
+ ///
+ public const int DefaultMaximumIterations = 99;
+
+ ///
+ /// The maximum number of agent interactions for a given chat invocation.
+ /// Defaults to: .
+ ///
+ public int MaximumIterations { get; set; } = DefaultMaximumIterations;
+
+ ///
+ /// Set to have automatically clear if caller
+ /// proceeds with invocation subsequent to achieving termination criteria.
+ ///
+ public bool AutomaticReset { get; set; }
+
+ ///
+ /// Set of agents for which this strategy is applicable. If not set,
+ /// any agent is evaluated.
+ ///
+ public IReadOnlyList? Agents { get; set; }
+
+ ///
+ /// Called to evaluate termination once is evaluated.
+ ///
+ protected abstract Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken);
+
+ ///
+ /// Evaluate the input message and determine if the chat has met its completion criteria.
+ ///
+ /// The agent actively interacting with the nexus.
+ /// The most recent message
+ /// The to monitor for cancellation requests. The default is .
+ /// True to terminate chat loop.
+ public Task ShouldTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken = default)
+ {
+ // `Agents` must contain `agent`, if `Agents` not empty.
+ if ((this.Agents?.Count ?? 0) > 0 && !this.Agents!.Any(a => a.Id == agent.Id))
+ {
+ return Task.FromResult(false);
+ }
+
+ return this.ShouldAgentTerminateAsync(agent, history, cancellationToken);
+ }
+}
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index 06a9b985db83..98a216daf821 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -1,5 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
-
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
index b3d5461f426c..a3d047e1bade 100644
--- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
+++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
@@ -3,7 +3,7 @@
SemanticKernel.Agents.UnitTests
SemanticKernel.Agents.UnitTests
- net6.0
+ net8.0
LatestMajor
true
false
diff --git a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs
new file mode 100644
index 000000000000..48b652491f53
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs
@@ -0,0 +1,221 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.Chat;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Core;
+
+///
+/// Unit testing of .
+///
+public class AgentGroupChatTests
+{
+ ///
+ /// Verify the default state of .
+ ///
+ [Fact]
+ public void VerifyGroupAgentChatDefaultState()
+ {
+ AgentGroupChat chat = new();
+ Assert.Empty(chat.Agents);
+ Assert.NotNull(chat.ExecutionSettings);
+ Assert.False(chat.IsComplete);
+
+ chat.IsComplete = true;
+ Assert.True(chat.IsComplete);
+ }
+
+ ///
+ /// Verify the management of instances as they join .
+ ///
+ [Fact]
+ public async Task VerifyGroupAgentChatAgentMembershipAsync()
+ {
+ Agent agent1 = CreateMockAgent().Object;
+ Agent agent2 = CreateMockAgent().Object;
+ Agent agent3 = CreateMockAgent().Object;
+ Agent agent4 = CreateMockAgent().Object;
+
+ AgentGroupChat chat = new(agent1, agent2);
+ Assert.Equal(2, chat.Agents.Count);
+
+ chat.AddAgent(agent3);
+ Assert.Equal(3, chat.Agents.Count);
+
+ var messages = await chat.InvokeAsync(agent4, isJoining: false).ToArrayAsync();
+ Assert.Equal(3, chat.Agents.Count);
+
+ messages = await chat.InvokeAsync(agent4).ToArrayAsync();
+ Assert.Equal(4, chat.Agents.Count);
+ }
+
+ ///
+ /// Verify the management of instances as they join .
+ ///
+ [Fact]
+ public async Task VerifyGroupAgentChatMultiTurnAsync()
+ {
+ Agent agent1 = CreateMockAgent().Object;
+ Agent agent2 = CreateMockAgent().Object;
+ Agent agent3 = CreateMockAgent().Object;
+
+ AgentGroupChat chat =
+ new(agent1, agent2, agent3)
+ {
+ ExecutionSettings =
+ new()
+ {
+ TerminationStrategy =
+ {
+ // This test is designed to take 9 turns.
+ MaximumIterations = 9,
+ }
+ },
+ IsComplete = true
+ };
+
+ await Assert.ThrowsAsync(() => chat.InvokeAsync(CancellationToken.None).ToArrayAsync().AsTask());
+
+ chat.ExecutionSettings.TerminationStrategy.AutomaticReset = true;
+ var messages = await chat.InvokeAsync(CancellationToken.None).ToArrayAsync();
+ Assert.Equal(9, messages.Length);
+ Assert.False(chat.IsComplete);
+
+ for (int index = 0; index < messages.Length; ++index) // Clean-up
+ {
+ switch (index % 3)
+ {
+ case 0:
+ Assert.Equal(agent1.Name, messages[index].AuthorName);
+ break;
+ case 1:
+ Assert.Equal(agent2.Name, messages[index].AuthorName);
+ break;
+ case 2:
+ Assert.Equal(agent3.Name, messages[index].AuthorName);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Verify the management of instances as they join .
+ ///
+ [Fact]
+ public async Task VerifyGroupAgentChatFailedSelectionAsync()
+ {
+ AgentGroupChat chat = Create3AgentChat();
+
+ chat.ExecutionSettings =
+ new()
+ {
+ // Strategy that will not select an agent.
+ SelectionStrategy = new FailedSelectionStrategy(),
+ TerminationStrategy =
+ {
+ // Remove max-limit in order to isolate the target behavior.
+ MaximumIterations = int.MaxValue
+ }
+ };
+
+ // Remove max-limit in order to isolate the target behavior.
+ chat.ExecutionSettings.TerminationStrategy.MaximumIterations = int.MaxValue;
+
+ await Assert.ThrowsAsync(() => chat.InvokeAsync().ToArrayAsync().AsTask());
+ }
+
+ ///
+ /// Verify the management of instances as they join .
+ ///
+ [Fact]
+ public async Task VerifyGroupAgentChatMultiTurnTerminationAsync()
+ {
+ AgentGroupChat chat = Create3AgentChat();
+
+ chat.ExecutionSettings =
+ new()
+ {
+ TerminationStrategy =
+ new TestTerminationStrategy(shouldTerminate: true)
+ {
+ // Remove max-limit in order to isolate the target behavior.
+ MaximumIterations = int.MaxValue
+ }
+ };
+
+ var messages = await chat.InvokeAsync(CancellationToken.None).ToArrayAsync();
+ Assert.Single(messages);
+ Assert.True(chat.IsComplete);
+ }
+
+ ///
+ /// Verify the management of instances as they join .
+ ///
+ [Fact]
+ public async Task VerifyGroupAgentChatDiscreteTerminationAsync()
+ {
+ Agent agent1 = CreateMockAgent().Object;
+
+ AgentGroupChat chat =
+ new()
+ {
+ ExecutionSettings =
+ new()
+ {
+ TerminationStrategy =
+ new TestTerminationStrategy(shouldTerminate: true)
+ {
+ // Remove max-limit in order to isolate the target behavior.
+ MaximumIterations = int.MaxValue
+ }
+ }
+ };
+
+ var messages = await chat.InvokeAsync(agent1).ToArrayAsync();
+ Assert.Single(messages);
+ Assert.True(chat.IsComplete);
+ }
+
+ private static AgentGroupChat Create3AgentChat()
+ {
+ Agent agent1 = CreateMockAgent().Object;
+ Agent agent2 = CreateMockAgent().Object;
+ Agent agent3 = CreateMockAgent().Object;
+
+ return new(agent1, agent2, agent3);
+ }
+
+ private static Mock CreateMockAgent()
+ {
+ Mock agent = new();
+
+ ChatMessageContent[] messages = [new ChatMessageContent(AuthorRole.Assistant, "test")];
+ agent.Setup(a => a.InvokeAsync(It.IsAny>(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable());
+
+ return agent;
+ }
+
+ private sealed class TestTerminationStrategy(bool shouldTerminate) : TerminationStrategy
+ {
+ protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(shouldTerminate);
+ }
+ }
+
+ private sealed class FailedSelectionStrategy : SelectionStrategy
+ {
+ public override Task NextAsync(IReadOnlyList agents, IReadOnlyList history, CancellationToken cancellationToken = default)
+ {
+ throw new InvalidOperationException();
+ }
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs
new file mode 100644
index 000000000000..d17391ee24be
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.SemanticKernel.Agents.Chat;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Core.Chat;
+
+///
+/// Unit testing of .
+///
+public class AgentGroupChatSettingsTests
+{
+ ///
+ /// Verify default state.
+ ///
+ [Fact]
+ public void VerifyChatExecutionSettingsDefault()
+ {
+ AgentGroupChatSettings settings = new();
+ Assert.IsType(settings.TerminationStrategy);
+ Assert.Equal(1, settings.TerminationStrategy.MaximumIterations);
+ Assert.IsType(settings.SelectionStrategy);
+ }
+
+ ///
+ /// Verify accepts for .
+ ///
+ [Fact]
+ public void VerifyChatExecutionContinuationStrategyDefault()
+ {
+ Mock strategyMock = new();
+ AgentGroupChatSettings settings =
+ new()
+ {
+ TerminationStrategy = strategyMock.Object
+ };
+
+ Assert.Equal(strategyMock.Object, settings.TerminationStrategy);
+ }
+
+ ///
+ /// Verify accepts for .
+ ///
+ [Fact]
+ public void VerifyChatExecutionSelectionStrategyDefault()
+ {
+ Mock strategyMock = new();
+ AgentGroupChatSettings settings =
+ new()
+ {
+ SelectionStrategy = strategyMock.Object
+ };
+
+ Assert.NotNull(settings.SelectionStrategy);
+ Assert.Equal(strategyMock.Object, settings.SelectionStrategy);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs
new file mode 100644
index 000000000000..192c3f846ec2
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.Chat;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Core.Chat;
+
+///
+/// Unit testing of .
+///
+public class AggregatorTerminationStrategyTests
+{
+ ///
+ /// Verify initial state.
+ ///
+ [Fact]
+ public void VerifyAggregateTerminationStrategyInitialState()
+ {
+ AggregatorTerminationStrategy strategy = new();
+ Assert.Equal(AggregateTerminationCondition.All, strategy.Condition);
+ }
+
+ ///
+ /// Verify evaluation of AggregateTerminationCondition.Any.
+ ///
+ [Fact]
+ public async Task VerifyAggregateTerminationStrategyAnyAsync()
+ {
+ TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true);
+ TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false);
+
+ Mock agentMock = new();
+
+ await VerifyResultAsync(
+ expectedResult: true,
+ agentMock.Object,
+ new(strategyMockTrue, strategyMockFalse)
+ {
+ Condition = AggregateTerminationCondition.Any,
+ });
+
+ await VerifyResultAsync(
+ expectedResult: false,
+ agentMock.Object,
+ new(strategyMockFalse, strategyMockFalse)
+ {
+ Condition = AggregateTerminationCondition.Any,
+ });
+
+ await VerifyResultAsync(
+ expectedResult: true,
+ agentMock.Object,
+ new(strategyMockTrue, strategyMockTrue)
+ {
+ Condition = AggregateTerminationCondition.Any,
+ });
+ }
+
+ ///
+ /// Verify evaluation of AggregateTerminationCondition.All.
+ ///
+ [Fact]
+ public async Task VerifyAggregateTerminationStrategyAllAsync()
+ {
+ TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true);
+ TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false);
+
+ Mock agentMock = new();
+
+ await VerifyResultAsync(
+ expectedResult: false,
+ agentMock.Object,
+ new(strategyMockTrue, strategyMockFalse)
+ {
+ Condition = AggregateTerminationCondition.All,
+ });
+
+ await VerifyResultAsync(
+ expectedResult: false,
+ agentMock.Object,
+ new(strategyMockFalse, strategyMockFalse)
+ {
+ Condition = AggregateTerminationCondition.All,
+ });
+
+ await VerifyResultAsync(
+ expectedResult: true,
+ agentMock.Object,
+ new(strategyMockTrue, strategyMockTrue)
+ {
+ Condition = AggregateTerminationCondition.All,
+ });
+ }
+
+ ///
+ /// Verify evaluation of agent scope evaluation.
+ ///
+ [Fact]
+ public async Task VerifyAggregateTerminationStrategyAgentAsync()
+ {
+ TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true);
+ TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false);
+
+ Mock agentMockA = new();
+ Mock agentMockB = new();
+
+ await VerifyResultAsync(
+ expectedResult: false,
+ agentMockB.Object,
+ new(strategyMockTrue, strategyMockTrue)
+ {
+ Agents = new[] { agentMockA.Object },
+ Condition = AggregateTerminationCondition.All,
+ });
+
+ await VerifyResultAsync(
+ expectedResult: true,
+ agentMockB.Object,
+ new(strategyMockTrue, strategyMockTrue)
+ {
+ Agents = new[] { agentMockB.Object },
+ Condition = AggregateTerminationCondition.All,
+ });
+ }
+
+ private static async Task VerifyResultAsync(bool expectedResult, Agent agent, AggregatorTerminationStrategy strategyRoot)
+ {
+ var result = await strategyRoot.ShouldTerminateAsync(agent, Array.Empty());
+ Assert.Equal(expectedResult, result);
+ }
+
+ ///
+ /// Less side-effects when mocking protected method.
+ ///
+ private sealed class MockTerminationStrategy(bool terminationResult) : TerminationStrategy
+ {
+ protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken)
+ => Task.FromResult(terminationResult);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs
new file mode 100644
index 000000000000..08d29c95115e
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.Chat;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Core.Chat;
+
+///
+/// Unit testing of .
+///
+public class RegExTerminationStrategyTests
+{
+ ///
+ /// Verify abililty of strategy to match expression.
+ ///
+ [Fact]
+ public async Task VerifyExpressionTerminationStrategyAsync()
+ {
+ RegExTerminationStrategy strategy = new("test");
+
+ await VerifyResultAsync(
+ expectedResult: false,
+ new("(?:^|\\W)test(?:$|\\W)"),
+ content: "fred");
+
+ await VerifyResultAsync(
+ expectedResult: true,
+ new("(?:^|\\W)test(?:$|\\W)"),
+ content: "this is a test");
+ }
+
+ private static async Task VerifyResultAsync(bool expectedResult, RegExTerminationStrategy strategyRoot, string content)
+ {
+ ChatMessageContent message = new(AuthorRole.Assistant, content);
+ Mock agent = new();
+ var result = await strategyRoot.ShouldTerminateAsync(agent.Object, new[] { message });
+ Assert.Equal(expectedResult, result);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs
new file mode 100644
index 000000000000..04339a8309e4
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.Chat;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.Core.Chat;
+
+///
+/// Unit testing of .
+///
+public class SequentialSelectionStrategyTests
+{
+ ///
+ /// Verify provides agents in expected order.
+ ///
+ [Fact]
+ public async Task VerifySequentialSelectionStrategyTurnsAsync()
+ {
+ Mock agent1 = new();
+ Mock agent2 = new();
+
+ Agent[] agents = [agent1.Object, agent2.Object];
+ SequentialSelectionStrategy strategy = new();
+
+ await VerifyNextAgent(agent1.Object);
+ await VerifyNextAgent(agent2.Object);
+ await VerifyNextAgent(agent1.Object);
+ await VerifyNextAgent(agent2.Object);
+ await VerifyNextAgent(agent1.Object);
+
+ strategy.Reset();
+ await VerifyNextAgent(agent1.Object);
+
+ // Verify index does not exceed current bounds.
+ agents = [agent1.Object];
+ await VerifyNextAgent(agent1.Object);
+
+ async Task VerifyNextAgent(Agent agent1)
+ {
+ Agent? nextAgent = await strategy.NextAsync(agents, []);
+ Assert.NotNull(nextAgent);
+ Assert.Equal(agent1.Id, nextAgent.Id);
+ }
+ }
+
+ ///
+ /// Verify behavior with no agents.
+ ///
+ [Fact]
+ public async Task VerifySequentialSelectionStrategyEmptyAsync()
+ {
+ SequentialSelectionStrategy strategy = new();
+ await Assert.ThrowsAsync(() => strategy.NextAsync([], []));
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
index fea77fb4b299..5357f0edbd11 100644
--- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
@@ -54,11 +54,15 @@ public async Task VerifyChatCompletionAgentInvocationAsync()
var agent =
new ChatCompletionAgent()
{
- Kernel = CreateKernel(mockService.Object)
+ Instructions = "test instructions",
+ Kernel = CreateKernel(mockService.Object),
+ ExecutionSettings = new(),
};
var result = await agent.InvokeAsync([]).ToArrayAsync();
+ Assert.Single(result);
+
mockService.Verify(
x =>
x.GetChatMessageContentsAsync(