diff --git a/dotnet/src/Microsoft.AutoGen/AgentChat/Abstractions/ChatAgent.cs b/dotnet/src/Microsoft.AutoGen/AgentChat/Abstractions/ChatAgent.cs
index 9fd5d5c1e8b0..6cad4f88dfe2 100644
--- a/dotnet/src/Microsoft.AutoGen/AgentChat/Abstractions/ChatAgent.cs
+++ b/dotnet/src/Microsoft.AutoGen/AgentChat/Abstractions/ChatAgent.cs
@@ -90,13 +90,13 @@ public struct AgentName
private static readonly Regex AgentNameRegex = new Regex($"^{IdStartClass}{IdContinueClass}*$", RegexOptions.Compiled | RegexOptions.Singleline);
- public string Name { get; }
+ public string Value { get; }
public AgentName(string name)
{
AgentName.CheckValid(name);
- this.Name = name;
+ this.Value = name;
}
public static bool IsValid(string name) => AgentNameRegex.IsMatch(name);
@@ -110,7 +110,7 @@ public static void CheckValid(string name)
}
// Implicit cast to string
- public static implicit operator string(AgentName agentName) => agentName.Name;
+ public static implicit operator string(AgentName agentName) => agentName.Value;
}
///
diff --git a/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/ChatAgentRouter.cs b/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/ChatAgentRouter.cs
new file mode 100644
index 000000000000..a195cd4d6743
--- /dev/null
+++ b/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/ChatAgentRouter.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ChatAgentRouter.cs
+
+using Microsoft.AutoGen.AgentChat.Abstractions;
+using Microsoft.AutoGen.Contracts;
+using Microsoft.AutoGen.Core;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AutoGen.AgentChat.GroupChat;
+
+public struct AgentChatConfig(IChatAgent chatAgent, string parentTopicType, string outputTopicType)
+{
+ public string ParticipantTopicType => this.Name;
+ public string ParentTopicType { get; } = parentTopicType;
+ public string OutputTopicType { get; } = outputTopicType;
+
+ public IChatAgent ChatAgent { get; } = chatAgent;
+
+ public string Name => this.ChatAgent.Name;
+ public string Description => this.ChatAgent.Description;
+}
+
+internal sealed class ChatAgentRouter : HostableAgentAdapter,
+ IHandle,
+ IHandle,
+ IHandle,
+ IHandle
+{
+ private readonly TopicId parentTopic;
+ private readonly TopicId outputTopic;
+ private readonly IChatAgent agent;
+
+ public ChatAgentRouter(AgentInstantiationContext agentCtx, AgentChatConfig config, ILogger? logger = null) : base(agentCtx, config.Description, logger)
+ {
+ this.parentTopic = new TopicId(config.ParentTopicType, this.Id.Key);
+ this.outputTopic = new TopicId(config.OutputTopicType, this.Id.Key);
+
+ this.agent = config.ChatAgent;
+ }
+
+ public List MessageBuffer { get; private set; } = new();
+
+ public ValueTask HandleAsync(GroupChatStart item, MessageContext messageContext)
+ {
+ if (item.Messages != null)
+ {
+ this.MessageBuffer.AddRange(item.Messages);
+ }
+
+ return ValueTask.CompletedTask;
+ }
+
+ public ValueTask HandleAsync(GroupChatAgentResponse item, MessageContext messageContext)
+ {
+ this.MessageBuffer.Add(item.AgentResponse.Message);
+
+ return ValueTask.CompletedTask;
+ }
+
+ public async ValueTask HandleAsync(GroupChatRequestPublish item, MessageContext messageContext)
+ {
+ Response? response = null;
+
+ // TODO: Is there a better abstraction here than IAsyncEnumerable? Though the akwardness mainly comes from
+ // the lack of real type unions in C#, which is why we need to create the StreamingFrame type in the first
+ // place.
+ await foreach (ChatStreamFrame frame in this.agent.StreamAsync(this.MessageBuffer, messageContext.CancellationToken))
+ {
+ // TODO: call publish message
+ switch (frame.Type)
+ {
+ case ChatStreamFrame.FrameType.Response:
+ await this.PublishMessageAsync(new GroupChatMessage { Message = frame.Response!.Message }, this.outputTopic);
+ response = frame.Response;
+ break;
+ case ChatStreamFrame.FrameType.InternalMessage:
+ await this.PublishMessageAsync(new GroupChatMessage { Message = frame.InternalMessage! }, this.outputTopic);
+ break;
+ }
+ }
+
+ if (response == null)
+ {
+ throw new InvalidOperationException("The agent did not produce a final response. Check the agent's on_messages_stream method.");
+ }
+
+ this.MessageBuffer.Clear();
+
+ await this.PublishMessageAsync(new GroupChatAgentResponse { AgentResponse = response }, this.parentTopic);
+ }
+
+ public ValueTask HandleAsync(GroupChatReset item, MessageContext messageContext)
+ {
+ this.MessageBuffer.Clear();
+ return this.agent.ResetAsync(messageContext.CancellationToken);
+ }
+}
+
diff --git a/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/GroupChatBase.cs b/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/GroupChatBase.cs
index 9a1fd536703c..bfc2faa1b422 100644
--- a/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/GroupChatBase.cs
+++ b/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/GroupChatBase.cs
@@ -1,37 +1,235 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// GroupChatBase.cs
+using System.Diagnostics;
+using System.Reflection;
+using System.Runtime.CompilerServices;
using Microsoft.AutoGen.AgentChat.Abstractions;
+using Microsoft.AutoGen.Contracts;
+using Microsoft.AutoGen.Core;
namespace Microsoft.AutoGen.AgentChat.GroupChat;
+internal static class AgentsRuntimeExtensions
+{
+ public static async ValueTask RegisterChatAgentAsync(this IAgentRuntime runtime, AgentChatConfig config)
+ {
+ AgentType type = config.Name;
+
+ AgentType resultType = await runtime.RegisterAgentFactoryAsync(type,
+ (id, runtime) =>
+ {
+ AgentInstantiationContext agentContext = new AgentInstantiationContext(id, runtime);
+ return ValueTask.FromResult(new ChatAgentRouter(agentContext, config));
+ });
+
+ await runtime.AddSubscriptionAsync(new TypeSubscription(config.ParticipantTopicType, type));
+ await runtime.AddSubscriptionAsync(new TypeSubscription(config.ParentTopicType, type));
+
+ return resultType;
+ }
+
+ public static async ValueTask RegisterGroupChatManagerAsync(this IAgentRuntime runtime, GroupChatOptions options, string teamId, Func factory)
+ where TManager : GroupChatManagerBase
+ {
+ AgentType type = GroupChatBase.GroupChatManagerTopicType;
+ AgentId expectedId = new AgentId(type, teamId);
+
+ AgentType resultType = await runtime.RegisterAgentFactoryAsync(type,
+ (id, runtime) =>
+ {
+ Debug.Assert(expectedId == id, $"Expecting the AgentId {expectedId} to be the teamId {id}");
+
+ AgentInstantiationContext agentContext = new AgentInstantiationContext(id, runtime);
+ TManager gcm = factory(options); // TODO: Should we allow this to be async?
+
+ return ValueTask.FromResult(new GroupChatHandlerRouter(agentContext, gcm));
+ });
+
+ await runtime.AddSubscriptionAsync(new TypeSubscription(GroupChatBase.GroupChatManagerTopicType, resultType));
+ await runtime.AddSubscriptionAsync(new TypeSubscription(options.GroupChatTopicType, resultType));
+
+ return resultType;
+ }
+
+ public static async ValueTask RegisterOutputCollectorAsync(this IAgentRuntime runtime, IOutputCollectionSink sink, string outputTopicType)
+ {
+ AgentType type = GroupChatBase.CollectorAgentType;
+ AgentType resultType = await runtime.RegisterAgentFactoryAsync(type,
+ (id, runtime) =>
+ {
+ AgentInstantiationContext agentContext = new AgentInstantiationContext(id, runtime);
+ return ValueTask.FromResult(new OutputCollectorAgent(agentContext, sink));
+ });
+
+ await runtime.AddSubscriptionAsync(new TypeSubscription(outputTopicType, type));
+
+ return resultType;
+ }
+}
+
public abstract class GroupChatBase : ITeam where TManager : GroupChatManagerBase
{
- public GroupChatBase(List participants, ITerminationCondition? terminationCondition = null, int? maxTurns = null)
+ // TODO: Where do these come from?
+ internal const string GroupTopicType = "group_topic";
+ internal const string OutputTopicType = "output_topic";
+ internal const string GroupChatManagerTopicType = "group_chat_manager";
+ internal const string CollectorAgentType = "collect_output_messages";
+
+ private GroupChatOptions GroupChatOptions { get; }
+
+ private readonly List messageThread = new();
+ private Dictionary Participants { get; } = new();
+
+ protected GroupChatBase(List participants, ITerminationCondition? terminationCondition = null, int? maxTurns = null)
{
- this.TeamId = Guid.NewGuid();
+ this.GroupChatOptions = new GroupChatOptions(GroupTopicType, OutputTopicType)
+ {
+ TerminationCondition = terminationCondition,
+ MaxTurns = maxTurns,
+ };
+
+ foreach (var participant in participants)
+ {
+ AgentChatConfig config = new AgentChatConfig(participant, GroupTopicType, OutputTopicType);
+ this.Participants[participant.Name] = config;
+ this.GroupChatOptions.Participants[participant.Name] = (config.ParticipantTopicType, participant.Description);
+ }
+
+ this.messageThread = new List(); // TODO: Allow injecting this
+
+ this.TeamId = Guid.NewGuid().ToString().ToLowerInvariant();
}
- ///
- /// Gets the team id.
- ///
- public Guid TeamId
+ public string TeamId
{
get;
private set;
}
- /// />/>
+ public virtual TManager CreateChatManager(GroupChatOptions options)
+ {
+ try
+ {
+ if (Activator.CreateInstance(typeof(TManager), options) is TManager result)
+ {
+ return result;
+ };
+ }
+ catch (TargetInvocationException tie)
+ {
+ throw new Exception("Could not create chat manager", tie.InnerException);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("Could not create chat manager", ex);
+ }
+
+ throw new Exception("Could not create chat manager; make sure that it contains a ctor() or ctor(GroupChatOptions), or override the CreateChatManager method");
+ }
+
+ // TODO: Turn this into an IDisposable-based utility
+ private int running; // = 0
+ private bool EnsureSingleRun()
+ {
+ return Interlocked.CompareExchange(ref running, 1, 0) == 0;
+ }
+
+ private void EndRun()
+ {
+ this.running = 0;
+ }
+
+ public IAsyncEnumerable StreamAsync(string task, CancellationToken cancellationToken)
+ {
+ if (String.IsNullOrEmpty(task))
+ {
+ throw new ArgumentNullException(nameof(task));
+ }
+
+ // TODO: Send this on
+ TextMessage taskStart = new()
+ {
+ Content = task,
+ Source = "user"
+ };
+
+ return this.StreamAsync(taskStart, cancellationToken);
+ }
+
public ValueTask ResetAsync(CancellationToken cancel)
{
- throw new NotImplementedException();
+ return ValueTask.CompletedTask;
}
- /// />/>
- public IAsyncEnumerable StreamAsync(ChatMessage? task, CancellationToken cancellationToken = default)
+ public async IAsyncEnumerable StreamAsync(ChatMessage? task, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- task = task ?? throw new ArgumentNullException(nameof(task));
+ if (task == null)
+ {
+ throw new ArgumentNullException(nameof(task));
+ }
+
+ if (!this.EnsureSingleRun())
+ {
+ throw new InvalidOperationException("The task is already running.");
+ }
+
+ // TODO: How do we allow the user to configure this?
+ //AgentsAppBuilder builder = new AgentsAppBuilder().UseInProcessRuntime();
+ InProcessRuntime runtime = new InProcessRuntime();
+
+ foreach (AgentChatConfig config in this.Participants.Values)
+ {
+ await runtime.RegisterChatAgentAsync(config);
+ }
+
+ await runtime.RegisterGroupChatManagerAsync(this.GroupChatOptions, this.TeamId, this.CreateChatManager);
+
+ OutputSink outputSink = new OutputSink();
+ await runtime.RegisterOutputCollectorAsync(outputSink, this.GroupChatOptions.OutputTopicType);
+
+ await runtime.StartAsync();
+
+ Task shutdownTask = Task.CompletedTask;
+
+ try
+ {
+ // TODO: Protos
+ GroupChatStart taskMessage = new GroupChatStart
+ {
+ Messages = [task]
+ };
+
+ List runMessages = new();
+
+ AgentId chatManagerId = new AgentId(GroupChatManagerTopicType, this.TeamId);
+ await runtime.SendMessageAsync(taskMessage, chatManagerId, cancellationToken: cancellationToken);
+
+ shutdownTask = Task.Run(runtime.RunUntilIdleAsync);
+
+ while (true)
+ {
+ OutputSink.SinkFrame frame = await outputSink.WaitForDataAsync(cancellationToken);
+ runMessages.AddRange(frame.Messages);
+
+ foreach (AgentMessage message in frame.Messages)
+ {
+ yield return new TaskFrame(message);
+ }
+
+ if (frame.IsTerminal)
+ {
+ TaskResult result = new TaskResult(runMessages);
+ yield return new TaskFrame(result);
+ break;
+ }
+ }
+ }
+ finally
+ {
+ this.EndRun();
- return this.StreamAsync(task, cancellationToken);
+ await shutdownTask;
+ }
}
}
diff --git a/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/GroupChatHandlerRouter.cs b/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/GroupChatHandlerRouter.cs
new file mode 100644
index 000000000000..9855dcafb7ea
--- /dev/null
+++ b/dotnet/src/Microsoft.AutoGen/AgentChat/GroupChat/GroupChatHandlerRouter.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// GroupChatHandlerRouter.cs
+
+using Microsoft.AutoGen.Contracts;
+using Microsoft.AutoGen.Core;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AutoGen.AgentChat.GroupChat;
+
+internal delegate ValueTask MessagePublishServicer(GroupChatEventBase event_, string topicType, CancellationToken cancellation = default);
+
+internal interface IGroupChatHandler : IHandle, IHandle, IHandle
public class MessageContext(string messageId, CancellationToken cancellationToken)
{
+ public MessageContext(CancellationToken cancellation) : this(Guid.NewGuid().ToString(), cancellation)
+ { }
+
///
/// Gets or sets the unique identifier for this message.
///
diff --git a/dotnet/test/Microsoft.AutoGen.AgentChat.Tests/AgentChatSmokeTest.cs b/dotnet/test/Microsoft.AutoGen.AgentChat.Tests/AgentChatSmokeTest.cs
new file mode 100644
index 000000000000..018f80cae38a
--- /dev/null
+++ b/dotnet/test/Microsoft.AutoGen.AgentChat.Tests/AgentChatSmokeTest.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// AgentChatSmokeTest.cs
+
+using Microsoft.AutoGen.AgentChat.Abstractions;
+using Microsoft.AutoGen.AgentChat.Agents;
+using Microsoft.AutoGen.AgentChat.GroupChat;
+using Microsoft.AutoGen.AgentChat.Terminations;
+using Xunit;
+
+namespace Microsoft.AutoGen.AgentChat.Tests;
+
+public class AgentChatSmokeTest
+{
+ public class SpeakMessageAgent : ChatAgentBase
+ {
+ public SpeakMessageAgent(string name, string description, string content) : base(name, description)
+ {
+ this.Content = content;
+ }
+
+ public string Content { get; private set; }
+
+ public override IEnumerable ProducedMessageTypes => [typeof(HandoffMessage)];
+
+ public override ValueTask HandleAsync(IEnumerable item, CancellationToken cancellationToken)
+ {
+ Response result = new()
+ {
+ Message = new TextMessage { Content = this.Content, Source = this.Name }
+ };
+
+ return ValueTask.FromResult(result);
+ }
+
+ public override ValueTask ResetAsync(CancellationToken cancellationToken)
+ {
+ return ValueTask.CompletedTask;
+ }
+ }
+
+ public class TerminatingAgent : ChatAgentBase
+ {
+ public List? IncomingMessages { get; private set; }
+
+ public TerminatingAgent(string name, string description) : base(name, description)
+ {
+ }
+
+ public override IEnumerable ProducedMessageTypes => [typeof(StopMessage)];
+
+ public override ValueTask HandleAsync(IEnumerable item, CancellationToken cancellationToken)
+ {
+ this.IncomingMessages = item.ToList();
+
+ string content = "Terminating";
+ if (item.Any())
+ {
+ ChatMessage lastMessage = item.Last();
+
+ switch (lastMessage)
+ {
+ case TextMessage textMessage:
+ content = $"Terminating; got: {textMessage.Content}";
+ break;
+ case HandoffMessage handoffMessage:
+ content = $"Terminating; got handoff: {handoffMessage.Context}";
+ break;
+ }
+ }
+
+ Response result = new()
+ {
+ Message = new StopMessage { Content = content, Source = this.Name }
+ };
+
+ return ValueTask.FromResult(result);
+ }
+
+ public override ValueTask ResetAsync(CancellationToken cancellationToken)
+ {
+ this.IncomingMessages = null;
+
+ return ValueTask.CompletedTask;
+ }
+ }
+
+ [Fact]
+ public async Task Test_RoundRobin_SpeakAndTerminating()
+ {
+ TerminatingAgent terminatingAgent = new("Terminate", "Terminate");
+
+ ITeam chat = new RoundRobinGroupChat(
+ [
+ new SpeakMessageAgent("Speak", "Speak", "Hello"),
+ terminatingAgent
+ ],
+ terminationCondition: new StopMessageTermination());
+
+ TaskResult result = await chat.RunAsync("");
+
+ Assert.Equal(3, result.Messages.Count);
+ Assert.Equal("", Assert.IsType(result.Messages[0]).Content);
+ Assert.Equal("Hello", Assert.IsType(result.Messages[1]).Content);
+ Assert.Equal("Terminating; got: Hello", Assert.IsType(result.Messages[2]).Content);
+ }
+}