Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET AgentChat Part 1: Abstractions, Base Classes, RoundRobin #5434

Merged
merged 6 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions dotnet/AutoGen.sln
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStartedGrpc", "sampl
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Core.Grpc.Tests", "test\Microsoft.AutoGen.Core.Grpc.Tests\Microsoft.AutoGen.Core.Grpc.Tests.csproj", "{23A028D3-5EB1-4FA0-9CD1-A1340B830579}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.AgentChat", "src\Microsoft.AutoGen\AgentChat\Microsoft.AutoGen.AgentChat.csproj", "{7F828599-56E8-4597-8F68-EE26FD631417}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.AgentChat.Tests", "test\Microsoft.AutoGen.AgentChat.Tests\Microsoft.AutoGen.AgentChat.Tests.csproj", "{217A4F86-8ADD-4998-90BA-880092A019F5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -300,16 +304,14 @@ Global
{70A8D4B5-D0A6-4098-A6F3-6ED274B65E7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70A8D4B5-D0A6-4098-A6F3-6ED274B65E7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70A8D4B5-D0A6-4098-A6F3-6ED274B65E7D}.Release|Any CPU.Build.0 = Release|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.CoreOnly|Any CPU.ActiveCfg = Debug|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.CoreOnly|Any CPU.Build.0 = Debug|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.Release|Any CPU.Build.0 = Release|Any CPU
{AAD593FE-A49B-425E-A9FE-A0022CD25E3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AAD593FE-A49B-425E-A9FE-A0022CD25E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AAD593FE-A49B-425E-A9FE-A0022CD25E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AAD593FE-A49B-425E-A9FE-A0022CD25E3D}.Release|Any CPU.Build.0 = Release|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}.Release|Any CPU.Build.0 = Release|Any CPU
{C3740DF1-18B1-4607-81E4-302F0308C848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3740DF1-18B1-4607-81E4-302F0308C848}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3740DF1-18B1-4607-81E4-302F0308C848}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -318,6 +320,14 @@ Global
{23A028D3-5EB1-4FA0-9CD1-A1340B830579}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23A028D3-5EB1-4FA0-9CD1-A1340B830579}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23A028D3-5EB1-4FA0-9CD1-A1340B830579}.Release|Any CPU.Build.0 = Release|Any CPU
{7F828599-56E8-4597-8F68-EE26FD631417}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F828599-56E8-4597-8F68-EE26FD631417}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F828599-56E8-4597-8F68-EE26FD631417}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F828599-56E8-4597-8F68-EE26FD631417}.Release|Any CPU.Build.0 = Release|Any CPU
{217A4F86-8ADD-4998-90BA-880092A019F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{217A4F86-8ADD-4998-90BA-880092A019F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{217A4F86-8ADD-4998-90BA-880092A019F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{217A4F86-8ADD-4998-90BA-880092A019F5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -368,11 +378,13 @@ Global
{EAFFE339-26CB-4019-991D-BCCE8E7D33A1} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
{58AD8E1D-83BD-4950-A324-1A20677D78D9} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
{70A8D4B5-D0A6-4098-A6F3-6ED274B65E7D} = {CE0AA8D5-12B8-4628-9589-DAD8CB0DDCF6}
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
{AAD593FE-A49B-425E-A9FE-A0022CD25E3D} = {F42F9C8E-7BD9-4687-9B63-AFFA461AF5C1}
{F42F9C8E-7BD9-4687-9B63-AFFA461AF5C1} = {CE0AA8D5-12B8-4628-9589-DAD8CB0DDCF6}
{3D83C6DB-ACEA-48F3-959F-145CCD2EE135} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
{C3740DF1-18B1-4607-81E4-302F0308C848} = {CE0AA8D5-12B8-4628-9589-DAD8CB0DDCF6}
{23A028D3-5EB1-4FA0-9CD1-A1340B830579} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
{7F828599-56E8-4597-8F68-EE26FD631417} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
{217A4F86-8ADD-4998-90BA-880092A019F5} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B}
Expand Down
193 changes: 193 additions & 0 deletions dotnet/src/Microsoft.AutoGen/AgentChat/Abstractions/ChatAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ChatAgent.cs

using System.Text.RegularExpressions;

namespace Microsoft.AutoGen.AgentChat.Abstractions;

/// <summary>
/// A valid name for an agent.
/// </summary>
/// <remarks>
/// To ensure parity with Python, we require agent names to be Python identifiers.
/// </remarks>
public struct AgentName
{
//
// TODO: Ensure that only valid C# identifiers can pass the validation on Python?

/*
From https://docs.python.org/3/reference/lexical_analysis.html#identifiers:
```
identifier ::= xid_start xid_continue*
id_start ::= <all characters in general categories Lu, Ll, Lt, Lm, Lo, Nl, the underscore, and characters with the Other_ID_Start property>
id_continue ::= <all characters in id_start, plus characters in the categories Mn, Mc, Nd, Pc and others with the Other_ID_Continue property>
xid_start ::= <all characters in id_start whose NFKC normalization is in "id_start xid_continue*">
xid_continue ::= <all characters in id_continue whose NFKC normalization is in "id_continue*">
```

Note: we are not going to deal with normalization; it would require a lot of effort for likely little gain
(this will mean that, strictly speaking, .NET will support a subset of the identifiers that Python does)

The Unicode category codes mentioned above stand for:

* Lu - uppercase letters
* Ll - lowercase letters
* Lt - titlecase letters
* Lm - modifier letters
* Lo - other letters
* Nl - letter numbers*
* Mn - nonspacing marks
* Mc - spacing combining marks*
* Nd - decimal numbers
* Pc - connector punctuations

Of these, most are captured by "word characters" in .NET, \w, only needing \p{Nl} and \p{Mc} to be added.
While Copilot /thinks/ that \p{Pc} is needed, it is not, as it is part of \w in .NET.

* Other_ID_Start - explicit list of characters in PropList.txt to support backwards compatibility
* Other_ID_Continue - likewise

# ================================================

1885..1886 ; Other_ID_Start # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA
2118 ; Other_ID_Start # Sm SCRIPT CAPITAL P
212E ; Other_ID_Start # So ESTIMATED SYMBOL
309B..309C ; Other_ID_Start # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK

# Total code points: 6

The pattern for this in .NET is [\u1185-\u1186\u2118\u212E\u309B-\u309C]

# ================================================

00B7 ; Other_ID_Continue # Po MIDDLE DOT
0387 ; Other_ID_Continue # Po GREEK ANO TELEIA
1369..1371 ; Other_ID_Continue # No [9] ETHIOPIC DIGIT ONE..ETHIOPIC DIGIT NINE
19DA ; Other_ID_Continue # No NEW TAI LUE THAM DIGIT ONE
200C..200D ; Other_ID_Continue # Cf [2] ZERO WIDTH NON-JOINER..ZERO WIDTH JOINER
30FB ; Other_ID_Continue # Po KATAKANA MIDDLE DOT
FF65 ; Other_ID_Continue # Po HALFWIDTH KATAKANA MIDDLE DOT

# Total code points: 16

The pattern for this in .NET is [\u00B7\u0387\u1369-\u1371\u19DA\u200C\u200D\u30FB\uFF65]

# ================================================

Classes for "IdStart": {Lu, Ll, Lt, Lm, Lo, Nl, '_', Other_ID_Start}
pattern: [\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}_\u1185-\u1186\u2118\u212E\u309B-\u309C]

Classes for "IdContinue": {\w, Nl, Mc, Other_ID_Start, Other_ID_Continue}
pattern: [\w\p{Nl}\p{Mc}_\u1185-\u1186\u2118\u212E\u309B-\u309C\u00B7\u0387\u1369-\u1371\u19DA\u200C\u200D\u30FB\uFF65]

Match group for identifiers:
(?<ident>(?:[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}_\u1185-\u1186\u2118\u212E\u309B-\u309C])(?:[\w\p{Nl}\p{Mc}_\u1185-\u1186\u2118\u212E\u309B-\u309C\u00B7\u0387\u1369-\u1371\u19DA\u200C\u200D\u30FB\uFF65])*)
*/

private const string IdStartClass = @"[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}_\u1185-\u1186\u2118\u212E\u309B-\u309C]";
private const string IdContinueClass = @"[\w\p{Nl}\p{Mc}_\u1185-\u1186\u2118\u212E\u309B-\u309C\u00B7\u0387\u1369-\u1371\u19DA\u200C\u200D\u30FB\uFF65]";

private static readonly Regex AgentNameRegex = new Regex($"^{IdStartClass}{IdContinueClass}*$", RegexOptions.Compiled | RegexOptions.Singleline);

public string Value { get; }

public AgentName(string name)
{
AgentName.CheckValid(name);

this.Value = name;
}

public static bool IsValid(string name) => AgentNameRegex.IsMatch(name);

public static void CheckValid(string name)
{
if (!AgentName.IsValid(name))
{
throw new ArgumentException($"Agent name '{name}' is not a valid identifier.");
}
}

// Implicit cast to string
public static implicit operator string(AgentName agentName) => agentName.Value;
}

/// <summary>
/// A response from calling <see cref="IChatAgent"/>'s <see cref="IHandleChat{TIn, Response}.HandleAsync(TIn)"/>."/>
/// </summary>
public class Response
{
/// <summary>
/// A chat message produced by the agent as a response.
/// </summary>
public required ChatMessage Message { get; set; }

/// <summary>
/// Inner messages produced by the agent.
/// </summary>
public List<AgentMessage>? InnerMessages { get; set; }
}

/// <summary>
/// Base class for representing a stream of messages interspacing responses (<typeparamref name="TResponse"/>) and
/// internal processing messages (<typeparamref name="TInternalMessage"/>). This functions as a discriminated union.
/// </summary>
/// <typeparam name="TResponse">The response type. Usually <see cref="Response"/>.</typeparam>
/// <typeparam name="TInternalMessage">The ineternal message type. Usually <see cref="AgentMessage"/>.</typeparam>
public class StreamingFrame<TResponse, TInternalMessage>() where TInternalMessage : AgentMessage
{
public enum FrameType
{
InternalMessage,
Response
}

public FrameType Type { get; set; }

public TInternalMessage? InternalMessage { get; set; }
public TResponse? Response { get; set; }
}

/// <summary>
/// Base class for representing a stream of messages with internal messages of any <see cref="AgentMessage"/> subtype.
/// </summary>
/// <typeparam name="TResponse">The response type. Usually <see cref="Response"/>.</typeparam>
public class StreamingFrame<TResponse> : StreamingFrame<TResponse, AgentMessage>;

/// <summary>
/// The stream frame for <see cref="IChatAgent"/>'s <see cref="IHandleStream{TIn, ChatStreamFrame}.StreamAsync(TIn)"/>
/// </summary>
public class ChatStreamFrame : StreamingFrame<Response, AgentMessage>;

/// <summary>
/// An agent that can participate in a chat.
/// </summary>
public interface IChatAgent :
IHandleChat<IEnumerable<ChatMessage>, Response>,
IHandleStream<IEnumerable<ChatMessage>, ChatStreamFrame>
{
/// <summary>
/// The name of the agent. This is used by team to uniquely identify the agent.It should be unique within the team.
/// </summary>
AgentName Name { get; }

/// <summary>
/// The description of the agent. This is used by team to make decisions about which agents to use.The description
/// should describe the agent's capabilities and how to interact with it.
/// </summary>
string Description { get; }

/// <summary>
/// The types of messages that the agent produces.
/// </summary>
IEnumerable<Type> ProducedMessageTypes { get; } // TODO: Is there a way to make this part of the type somehow?
// Annotations, or IProduce<>? Do we ever actually access this?

/// <summary>
/// Reset the agent to its initialization state.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
ValueTask ResetAsync(CancellationToken cancellationToken);
}
50 changes: 50 additions & 0 deletions dotnet/src/Microsoft.AutoGen/AgentChat/Abstractions/Handoff.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Handoff.cs

namespace Microsoft.AutoGen.AgentChat.Abstractions;

/// <summary>
/// Handoff configuration.
/// </summary>
/// <param name="target">The name of the target agent receiving the handoff.</param>
/// <param name="description">The description of the handoff such as the condition under which it should happen and the target
/// agent's ability. If not provided, it is generated from the target agent's name.</param>
/// <param name="name">The name of this handoff configuration. If not provided, it is generated from the target agent's name.</param>
/// <param name="message">The message to the target agent. If not provided, it is generated from the target agent's name.</param>
public class Handoff(string target, string? description = null, string? name = null, string? message = null)
{
private static string? CheckName(string? name)
{
if (name != null && !AgentName.IsValid(name))
{
throw new ArgumentException($"Handoff name '{name}' is not a valid identifier.");
}

return name;
}

/// <summary>
/// The name of the target agent receiving the handoff.
/// </summary>
public AgentName Target { get; } = new AgentName(target);

/// <summary>
/// The description of the handoff such as the condition under which it should happen and the target.
/// </summary>
public string Description { get; } = description ?? $"Handoff to {target}";

/// <summary>
/// The name of this handoff configuration.
/// </summary>
public string Name { get; } = CheckName(name) ?? $"transfer_to_{target.ToLowerInvariant()}";

/// <summary>
/// The content of the HandoffMessage that will be sent.
/// </summary>
public string Message { get; } = message ?? $"Transferred to {target}, adopting the role of {target} immediately.";

/// <summary>
/// Handoff Tool to execute the handoff.
/// </summary>
public ITool HandoffTool => new CallableTool(this.Name, this.Description, () => { return this.Message; });
}
17 changes: 17 additions & 0 deletions dotnet/src/Microsoft.AutoGen/AgentChat/Abstractions/ITeam.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ITeam.cs

namespace Microsoft.AutoGen.AgentChat.Abstractions;

/// <summary>
/// A team of agents.
/// </summary>
public interface ITeam : ITaskRunner
{
/// <summary>
/// Reset the team and all its participants to its initial state.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
ValueTask ResetAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// MessageHandling.cs

namespace Microsoft.AutoGen.AgentChat.Abstractions;

public interface IHandleChat<in TIn>
{
ValueTask HandleAsync(TIn item)
{
return this.HandleAsync(item, CancellationToken.None);
}

ValueTask HandleAsync(TIn item, CancellationToken cancellationToken);
}

public interface IHandleChat<in TIn, TOut> // TODO: Map this to IHandle<> somehow?
{
ValueTask<TOut> HandleAsync(TIn item)
{
return this.HandleAsync(item, CancellationToken.None);
}

ValueTask<TOut> HandleAsync(TIn item, CancellationToken cancellationToken);
}

public interface IHandleDefault : IHandleChat<object>
{
}

public interface IHandleStream<in TIn, TOut>
{
IAsyncEnumerable<TOut> StreamAsync(TIn item)
{
return this.StreamAsync(item, CancellationToken.None);
}

IAsyncEnumerable<TOut> StreamAsync(TIn item, CancellationToken cancellationToken);
}
Loading
Loading