diff --git a/README.md b/README.md index 179067fe9106..a85b33992373 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,6 @@ from autogen_agentchat.teams import RoundRobinGroupChat from autogen_agentchat.ui import Console from autogen_ext.models.openai import OpenAIChatCompletionClient from autogen_ext.agents.web_surfer import MultimodalWebSurfer - async def main() -> None: model_client = OpenAIChatCompletionClient(model="gpt-4o") assistant = AssistantAgent("assistant", model_client) diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln index d0707a89a7fd..76a759e37692 100644 --- a/dotnet/AutoGen.sln +++ b/dotnet/AutoGen.sln @@ -100,16 +100,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.WebAPI.Sample", "sa EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DevTeam", "DevTeam", "{05B9C173-6441-4DCA-9AC4-E897EF75F331}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.AgentHost", "samples\dev-team\DevTeam.AgentHost\DevTeam.AgentHost.csproj", "{462A357B-7BB9-4927-A9FD-4FB7675898E9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.Agents", "samples\dev-team\DevTeam.Agents\DevTeam.Agents.csproj", "{83BBB833-A2F0-4A4D-BA1B-8229FC9BCD4F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.AppHost", "samples\dev-team\DevTeam.AppHost\DevTeam.AppHost.csproj", "{63280C12-3BE3-4C4E-805E-584CDC6BC1F5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.Backend", "samples\dev-team\DevTeam.Backend\DevTeam.Backend.csproj", "{EDA3EF83-FC7F-4BCF-945D-B893620EE4B1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.Shared", "samples\dev-team\DevTeam.Shared\DevTeam.Shared.csproj", "{01F5D7C3-41EB-409C-9B77-A945C07FA7E8}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hello", "Hello", "{7EB336C2-7C0A-4BC8-80C6-A3173AB8DC45}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hello.AppHost", "samples\Hello\Hello.AppHost\Hello.AppHost.csproj", "{09A373A0-8169-409F-8C37-3FBC1654B122}" @@ -126,8 +120,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloAgentState", "samples\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AutoGen.Extensions.Aspire", "src\Microsoft.AutoGen\Extensions\Aspire\Microsoft.AutoGen.Extensions.Aspire.csproj", "{65059914-5527-4A00-9308-9FAF23D5E85A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AutoGen.Agents.Tests", "test\Microsoft.AutoGen.Agents.Tests\Microsoft.AutoGen.Agents.Tests.csproj", "{394FDAF8-74F9-4977-94A5-3371737EB774}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Integration.Tests", "test\Microsoft.AutoGen.Integration.Tests\Microsoft.AutoGen.Integration.Tests.csproj", "{D04C6153-8EAF-4E54-9852-52CEC1BE8D31}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloAgent.AppHost", "test\Microsoft.AutoGen.Integration.Tests.AppHosts\HelloAgent.AppHost\HelloAgent.AppHost.csproj", "{99D7766B-076F-4E6F-A8D2-3DF1DAFA2599}" @@ -142,6 +134,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Agents", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Autogen.AgentHost", "src\Microsoft.AutoGen\AgentHost\Microsoft.Autogen.AgentHost.csproj", "{4CB42139-DEE4-40B9-AA81-1E4CCAA2F338}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Runtime.Grpc.Tests", "test\Microsoft.AutoGen.Runtime.Grpc.Tests\Microsoft.AutoGen.Runtime.Grpc.Tests.csproj", "{0E7983BB-2602-421E-8B37-332E52870A10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Tests.Shared", "test\Microsoft.AutoGen.Tests.Shared\Microsoft.AutoGen.Tests.Shared.csproj", "{14F90F79-580E-454D-BA7A-ED6D9723020D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Core.Tests", "test\Microsoft.AutoGen.Core.Tests\Microsoft.AutoGen.Core.Tests.csproj", "{EAFFE339-26CB-4019-991D-BCCE8E7D33A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevTeam.ServiceDefaults", "samples\dev-team\DevTeam.ServiceDefaults\DevTeam.ServiceDefaults.csproj", "{599E1971-1DA9-453F-A7A8-42510BBC95C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Core.Grpc.Tests", "test\Microsoft.AutoGen.Core.Grpc.Tests\Microsoft.AutoGen.Core.Grpc.Tests.csproj", "{33A28A4B-123B-4416-9631-0F759B8D6172}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -296,14 +298,6 @@ Global {4385AFCF-AB4A-49B2-BEBA-D33C950E1EE6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4385AFCF-AB4A-49B2-BEBA-D33C950E1EE6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4385AFCF-AB4A-49B2-BEBA-D33C950E1EE6}.Release|Any CPU.Build.0 = Release|Any CPU - {462A357B-7BB9-4927-A9FD-4FB7675898E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {462A357B-7BB9-4927-A9FD-4FB7675898E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {462A357B-7BB9-4927-A9FD-4FB7675898E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {462A357B-7BB9-4927-A9FD-4FB7675898E9}.Release|Any CPU.Build.0 = Release|Any CPU - {83BBB833-A2F0-4A4D-BA1B-8229FC9BCD4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {83BBB833-A2F0-4A4D-BA1B-8229FC9BCD4F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {83BBB833-A2F0-4A4D-BA1B-8229FC9BCD4F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83BBB833-A2F0-4A4D-BA1B-8229FC9BCD4F}.Release|Any CPU.Build.0 = Release|Any CPU {63280C12-3BE3-4C4E-805E-584CDC6BC1F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {63280C12-3BE3-4C4E-805E-584CDC6BC1F5}.Debug|Any CPU.Build.0 = Debug|Any CPU {63280C12-3BE3-4C4E-805E-584CDC6BC1F5}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -312,10 +306,6 @@ Global {EDA3EF83-FC7F-4BCF-945D-B893620EE4B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDA3EF83-FC7F-4BCF-945D-B893620EE4B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDA3EF83-FC7F-4BCF-945D-B893620EE4B1}.Release|Any CPU.Build.0 = Release|Any CPU - {01F5D7C3-41EB-409C-9B77-A945C07FA7E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01F5D7C3-41EB-409C-9B77-A945C07FA7E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01F5D7C3-41EB-409C-9B77-A945C07FA7E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01F5D7C3-41EB-409C-9B77-A945C07FA7E8}.Release|Any CPU.Build.0 = Release|Any CPU {09A373A0-8169-409F-8C37-3FBC1654B122}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {09A373A0-8169-409F-8C37-3FBC1654B122}.Debug|Any CPU.Build.0 = Debug|Any CPU {09A373A0-8169-409F-8C37-3FBC1654B122}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -340,10 +330,6 @@ Global {65059914-5527-4A00-9308-9FAF23D5E85A}.Debug|Any CPU.Build.0 = Debug|Any CPU {65059914-5527-4A00-9308-9FAF23D5E85A}.Release|Any CPU.ActiveCfg = Release|Any CPU {65059914-5527-4A00-9308-9FAF23D5E85A}.Release|Any CPU.Build.0 = Release|Any CPU - {394FDAF8-74F9-4977-94A5-3371737EB774}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {394FDAF8-74F9-4977-94A5-3371737EB774}.Debug|Any CPU.Build.0 = Debug|Any CPU - {394FDAF8-74F9-4977-94A5-3371737EB774}.Release|Any CPU.ActiveCfg = Release|Any CPU - {394FDAF8-74F9-4977-94A5-3371737EB774}.Release|Any CPU.Build.0 = Release|Any CPU {D04C6153-8EAF-4E54-9852-52CEC1BE8D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D04C6153-8EAF-4E54-9852-52CEC1BE8D31}.Debug|Any CPU.Build.0 = Debug|Any CPU {D04C6153-8EAF-4E54-9852-52CEC1BE8D31}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -372,6 +358,26 @@ Global {4CB42139-DEE4-40B9-AA81-1E4CCAA2F338}.Debug|Any CPU.Build.0 = Debug|Any CPU {4CB42139-DEE4-40B9-AA81-1E4CCAA2F338}.Release|Any CPU.ActiveCfg = Release|Any CPU {4CB42139-DEE4-40B9-AA81-1E4CCAA2F338}.Release|Any CPU.Build.0 = Release|Any CPU + {0E7983BB-2602-421E-8B37-332E52870A10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E7983BB-2602-421E-8B37-332E52870A10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E7983BB-2602-421E-8B37-332E52870A10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E7983BB-2602-421E-8B37-332E52870A10}.Release|Any CPU.Build.0 = Release|Any CPU + {14F90F79-580E-454D-BA7A-ED6D9723020D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14F90F79-580E-454D-BA7A-ED6D9723020D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14F90F79-580E-454D-BA7A-ED6D9723020D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14F90F79-580E-454D-BA7A-ED6D9723020D}.Release|Any CPU.Build.0 = Release|Any CPU + {EAFFE339-26CB-4019-991D-BCCE8E7D33A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAFFE339-26CB-4019-991D-BCCE8E7D33A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAFFE339-26CB-4019-991D-BCCE8E7D33A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAFFE339-26CB-4019-991D-BCCE8E7D33A1}.Release|Any CPU.Build.0 = Release|Any CPU + {599E1971-1DA9-453F-A7A8-42510BBC95C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {599E1971-1DA9-453F-A7A8-42510BBC95C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {599E1971-1DA9-453F-A7A8-42510BBC95C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {599E1971-1DA9-453F-A7A8-42510BBC95C2}.Release|Any CPU.Build.0 = Release|Any CPU + {33A28A4B-123B-4416-9631-0F759B8D6172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33A28A4B-123B-4416-9631-0F759B8D6172}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33A28A4B-123B-4416-9631-0F759B8D6172}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33A28A4B-123B-4416-9631-0F759B8D6172}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -417,11 +423,8 @@ Global {CB8824F5-9475-451F-87E8-F2AEF2490A12} = {668726B9-77BC-45CF-B576-0F0773BF1615} {4385AFCF-AB4A-49B2-BEBA-D33C950E1EE6} = {668726B9-77BC-45CF-B576-0F0773BF1615} {05B9C173-6441-4DCA-9AC4-E897EF75F331} = {686480D7-8FEC-4ED3-9C5D-CEBE1057A7ED} - {462A357B-7BB9-4927-A9FD-4FB7675898E9} = {05B9C173-6441-4DCA-9AC4-E897EF75F331} - {83BBB833-A2F0-4A4D-BA1B-8229FC9BCD4F} = {05B9C173-6441-4DCA-9AC4-E897EF75F331} {63280C12-3BE3-4C4E-805E-584CDC6BC1F5} = {05B9C173-6441-4DCA-9AC4-E897EF75F331} {EDA3EF83-FC7F-4BCF-945D-B893620EE4B1} = {05B9C173-6441-4DCA-9AC4-E897EF75F331} - {01F5D7C3-41EB-409C-9B77-A945C07FA7E8} = {05B9C173-6441-4DCA-9AC4-E897EF75F331} {7EB336C2-7C0A-4BC8-80C6-A3173AB8DC45} = {686480D7-8FEC-4ED3-9C5D-CEBE1057A7ED} {09A373A0-8169-409F-8C37-3FBC1654B122} = {7EB336C2-7C0A-4BC8-80C6-A3173AB8DC45} {A20B9894-F352-4338-872A-F215A241D43D} = {7EB336C2-7C0A-4BC8-80C6-A3173AB8DC45} @@ -429,7 +432,6 @@ Global {97550E87-48C6-4EBF-85E1-413ABAE9DBFD} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {64EF61E7-00A6-4E5E-9808-62E10993A0E5} = {7EB336C2-7C0A-4BC8-80C6-A3173AB8DC45} {65059914-5527-4A00-9308-9FAF23D5E85A} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} - {394FDAF8-74F9-4977-94A5-3371737EB774} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {D04C6153-8EAF-4E54-9852-52CEC1BE8D31} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {99D7766B-076F-4E6F-A8D2-3DF1DAFA2599} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {7F60934B-3E59-48D0-B26D-04A39FEC13EF} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} @@ -437,6 +439,11 @@ Global {8457B68C-CC86-4A3F-8559-C1AE199EC366} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {3892C83E-7F5D-41DF-A88C-4854EAD38856} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {4CB42139-DEE4-40B9-AA81-1E4CCAA2F338} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {0E7983BB-2602-421E-8B37-332E52870A10} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {14F90F79-580E-454D-BA7A-ED6D9723020D} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {EAFFE339-26CB-4019-991D-BCCE8E7D33A1} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {599E1971-1DA9-453F-A7A8-42510BBC95C2} = {05B9C173-6441-4DCA-9AC4-E897EF75F331} + {33A28A4B-123B-4416-9631-0F759B8D6172} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B} diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 1e84c0badb2f..d0131825aab4 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -45,6 +45,7 @@ + @@ -83,6 +84,7 @@ + @@ -125,7 +127,6 @@ - \ No newline at end of file diff --git a/dotnet/samples/Hello/Hello.AppHost/Program.cs b/dotnet/samples/Hello/Hello.AppHost/Program.cs index 8230a5b7c2d9..31dbeb8c2db7 100644 --- a/dotnet/samples/Hello/Hello.AppHost/Program.cs +++ b/dotnet/samples/Hello/Hello.AppHost/Program.cs @@ -12,7 +12,7 @@ .WaitFor(backend); #pragma warning disable ASPIREHOSTINGPYTHON001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. // xlang is over http for now - in prod use TLS between containers -builder.AddPythonApp("HelloAgentsPython", "../../../../python/packages/autogen-core/samples/xlang/hello_python_agent", "hello_python_agent.py", "../../../../../.venv") +builder.AddPythonApp("HelloAgentsPython", "../../../../python/samples/core_xlang_hello_python_agent", "hello_python_agent.py", "../../.venv") .WithReference(backend) .WithEnvironment("AGENT_HOST", backend.GetEndpoint("http")) .WithEnvironment("STAY_ALIVE_ON_GOODBYE", "true") diff --git a/dotnet/samples/Hello/HelloAIAgents/HelloAIAgent.cs b/dotnet/samples/Hello/HelloAIAgents/HelloAIAgent.cs index 0e195ca5b1dd..ba71b31a2017 100644 --- a/dotnet/samples/Hello/HelloAIAgents/HelloAIAgent.cs +++ b/dotnet/samples/Hello/HelloAIAgents/HelloAIAgent.cs @@ -8,17 +8,15 @@ namespace Hello; [TopicSubscription("agents")] public class HelloAIAgent( - IAgentWorker worker, - [FromKeyedServices("EventTypes")] EventTypes typeRegistry, + [FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, IHostApplicationLifetime hostApplicationLifetime, IChatClient client) : HelloAgent( - worker, typeRegistry, hostApplicationLifetime), IHandle { // This Handle supercedes the one in the base class - public new async Task Handle(NewMessageReceived item) + public new async Task Handle(NewMessageReceived item, CancellationToken cancellationToken = default) { var prompt = "Please write a limerick greeting someone with the name " + item.Message; var response = await client.CompleteAsync(prompt); diff --git a/dotnet/samples/Hello/HelloAIAgents/Program.cs b/dotnet/samples/Hello/HelloAIAgents/Program.cs index f9780d62af98..035da0353f85 100644 --- a/dotnet/samples/Hello/HelloAIAgents/Program.cs +++ b/dotnet/samples/Hello/HelloAIAgents/Program.cs @@ -7,7 +7,7 @@ using Microsoft.AutoGen.Core; // send a message to the agent -var builder = WebApplication.CreateBuilder(); +var builder = new HostApplicationBuilder(); // put these in your environment or appsettings.json builder.Configuration["HelloAIAgents:ModelType"] = "azureopenai"; builder.Configuration["HelloAIAgents:LlmModelName"] = "gpt-3.5-turbo"; @@ -33,16 +33,14 @@ namespace Hello { [TopicSubscription("agents")] public class HelloAgent( - IAgentWorker worker, - [FromKeyedServices("EventTypes")] EventTypes typeRegistry, + [FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, IHostApplicationLifetime hostApplicationLifetime) : ConsoleAgent( - worker, typeRegistry), ISayHello, IHandle, IHandle { - public async Task Handle(NewMessageReceived item) + public async Task Handle(NewMessageReceived item, CancellationToken cancellationToken = default) { var response = await SayHello(item.Message).ConfigureAwait(false); var evt = new Output @@ -57,7 +55,7 @@ public async Task Handle(NewMessageReceived item) }; await PublishMessageAsync(goodbye).ConfigureAwait(false); } - public async Task Handle(ConversationClosed item) + public async Task Handle(ConversationClosed item, CancellationToken cancellationToken = default) { var goodbye = $"********************* {item.UserId} said {item.UserMessage} ************************"; var evt = new Output diff --git a/dotnet/samples/Hello/HelloAgent/HelloAgent.csproj b/dotnet/samples/Hello/HelloAgent/HelloAgent.csproj index 5067a673df4b..73d3f43dc0e4 100644 --- a/dotnet/samples/Hello/HelloAgent/HelloAgent.csproj +++ b/dotnet/samples/Hello/HelloAgent/HelloAgent.csproj @@ -15,6 +15,7 @@ + diff --git a/dotnet/samples/Hello/HelloAgent/Program.cs b/dotnet/samples/Hello/HelloAgent/Program.cs index dee73b1a47d3..70c3fde7a3e7 100644 --- a/dotnet/samples/Hello/HelloAgent/Program.cs +++ b/dotnet/samples/Hello/HelloAgent/Program.cs @@ -8,7 +8,7 @@ var local = true; if (Environment.GetEnvironmentVariable("AGENT_HOST") != null) { local = false; } -var app = await AgentsApp.PublishMessageAsync("HelloAgents", new NewMessageReceived +var app = await Microsoft.AutoGen.Core.Grpc.AgentsApp.PublishMessageAsync("HelloAgents", new NewMessageReceived { Message = "World" }, local: local).ConfigureAwait(false); @@ -18,9 +18,8 @@ namespace Hello { [TopicSubscription("agents")] public class HelloAgent( - IAgentWorker worker, IHostApplicationLifetime hostApplicationLifetime, - [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : Agent( - worker, + IHostApplicationLifetime hostApplicationLifetime, + [FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry) : Agent( typeRegistry), ISayHello, IHandleConsole, @@ -28,7 +27,7 @@ public class HelloAgent( IHandle, IHandle { - public async Task Handle(NewMessageReceived item) + public async Task Handle(NewMessageReceived item, CancellationToken cancellationToken) { var response = await SayHello(item.Message).ConfigureAwait(false); var evt = new Output { Message = response }; @@ -40,7 +39,7 @@ public async Task Handle(NewMessageReceived item) }; await PublishMessageAsync(goodbye).ConfigureAwait(false); } - public async Task Handle(ConversationClosed item) + public async Task Handle(ConversationClosed item, CancellationToken cancellationToken) { var goodbye = $"********************* {item.UserId} said {item.UserMessage} ************************"; var evt = new Output { Message = goodbye }; @@ -51,13 +50,13 @@ public async Task Handle(ConversationClosed item) } } - public async Task Handle(Shutdown item) + public async Task Handle(Shutdown item, CancellationToken cancellationToken) { Console.WriteLine("Shutting down..."); hostApplicationLifetime.StopApplication(); } - public async Task SayHello(string ask) + public async Task SayHello(string ask, CancellationToken cancellationToken = default) { var response = $"\n\n\n\n***************Hello {ask}**********************\n\n\n\n"; return response; @@ -65,6 +64,6 @@ public async Task SayHello(string ask) } public interface ISayHello { - public Task SayHello(string ask); + public Task SayHello(string ask, CancellationToken cancellationToken = default); } } diff --git a/dotnet/samples/Hello/HelloAgent/README.md b/dotnet/samples/Hello/HelloAgent/README.md index 53e3d6a65eba..968f454905c3 100644 --- a/dotnet/samples/Hello/HelloAgent/README.md +++ b/dotnet/samples/Hello/HelloAgent/README.md @@ -25,10 +25,10 @@ Flow Diagram: ```mermaid %%{init: {'theme':'forest'}}%% graph LR; - A[Main] --> |"PublishEventAsync(NewMessage('World'))"| B{"Handle(NewMessageReceived item)"} + A[Main] --> |"PublishEventAsync(NewMessage('World'))"| B{"Handle(NewMessageReceived item, CancellationToken cancellationToken = default)"} B --> |"PublishEventAsync(Output('***Hello, World***'))"| C[ConsoleAgent] C --> D{"WriteConsole()"} - B --> |"PublishEventAsync(ConversationClosed('Goodbye'))"| E{"Handle(ConversationClosed item)"} + B --> |"PublishEventAsync(ConversationClosed('Goodbye'))"| E{"Handle(ConversationClosed item, CancellationToken cancellationToken = default)"} B --> |"PublishEventAsync(Output('***Goodbye***'))"| C E --> F{"Shutdown()"} @@ -44,14 +44,14 @@ Within that event handler you may optionally *emit* new events, which are then s TopicSubscription("HelloAgents")] public class HelloAgent( iAgentWorker worker, - [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : ConsoleAgent( + [FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry) : ConsoleAgent( worker, typeRegistry), ISayHello, IHandle, IHandle { - public async Task Handle(NewMessageReceived item) + public async Task Handle(NewMessageReceived item, CancellationToken cancellationToken = default) { var response = await SayHello(item.Message).ConfigureAwait(false); var evt = new Output diff --git a/dotnet/samples/Hello/HelloAgentState/Program.cs b/dotnet/samples/Hello/HelloAgentState/Program.cs index dbb16c3bbb9b..013d70786551 100644 --- a/dotnet/samples/Hello/HelloAgentState/Program.cs +++ b/dotnet/samples/Hello/HelloAgentState/Program.cs @@ -18,10 +18,8 @@ namespace Hello { [TopicSubscription("agents")] public class HelloAgent( - IAgentWorker worker, IHostApplicationLifetime hostApplicationLifetime, - [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : Agent( - worker, + [FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry) : Agent( typeRegistry), IHandleConsole, IHandle, @@ -29,7 +27,7 @@ public class HelloAgent( IHandle { private AgentState? State { get; set; } - public async Task Handle(NewMessageReceived item) + public async Task Handle(NewMessageReceived item, CancellationToken cancellationToken = default) { var response = await SayHello(item.Message).ConfigureAwait(false); var evt = new Output @@ -57,7 +55,7 @@ await StoreAsync(new AgentState await PublishMessageAsync(new Shutdown { Message = this.AgentId.Key }).ConfigureAwait(false); } - public async Task Handle(ConversationClosed item) + public async Task Handle(ConversationClosed item, CancellationToken cancellationToken = default) { State = await ReadAsync(this.AgentId).ConfigureAwait(false); var state = JsonSerializer.Deserialize>(State.TextData) ?? new Dictionary { { "data", "No state data found" } }; @@ -74,7 +72,7 @@ await StoreAsync(new AgentState TextData = JsonSerializer.Serialize(state) }).ConfigureAwait(false); } - public async Task Handle(Shutdown item) + public async Task Handle(Shutdown item, CancellationToken cancellationToken = default) { string? workflow = null; // make sure the workflow is finished diff --git a/dotnet/samples/Hello/HelloAgentState/README.md b/dotnet/samples/Hello/HelloAgentState/README.md index f46c66df1e1d..801d79a7c8f0 100644 --- a/dotnet/samples/Hello/HelloAgentState/README.md +++ b/dotnet/samples/Hello/HelloAgentState/README.md @@ -25,10 +25,10 @@ Flow Diagram: ```mermaid %%{init: {'theme':'forest'}}%% graph LR; - A[Main] --> |"PublishEventAsync(NewMessage('World'))"| B{"Handle(NewMessageReceived item)"} + A[Main] --> |"PublishEventAsync(NewMessage('World'))"| B{"Handle(NewMessageReceived item, CancellationToken cancellationToken = default)"} B --> |"PublishEventAsync(Output('***Hello, World***'))"| C[ConsoleAgent] C --> D{"WriteConsole()"} - B --> |"PublishEventAsync(ConversationClosed('Goodbye'))"| E{"Handle(ConversationClosed item)"} + B --> |"PublishEventAsync(ConversationClosed('Goodbye'))"| E{"Handle(ConversationClosed item, CancellationToken cancellationToken = default)"} B --> |"PublishEventAsync(Output('***Goodbye***'))"| C E --> F{"Shutdown()"} @@ -44,14 +44,14 @@ Within that event handler you may optionally *emit* new events, which are then s TopicSubscription("HelloAgents")] public class HelloAgent( iAgentWorker worker, - [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : ConsoleAgent( + [FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry) : ConsoleAgent( worker, typeRegistry), ISayHello, IHandle, IHandle { - public async Task Handle(NewMessageReceived item) + public async Task Handle(NewMessageReceived item, CancellationToken cancellationToken = default) { var response = await SayHello(item.Message).ConfigureAwait(false); var evt = new Output diff --git a/dotnet/samples/dev-team/DevTeam.AgentHost/DevTeam.AgentHost.csproj b/dotnet/samples/dev-team/DevTeam.AgentHost/DevTeam.AgentHost.csproj deleted file mode 100644 index 4da4bfd8d7e6..000000000000 --- a/dotnet/samples/dev-team/DevTeam.AgentHost/DevTeam.AgentHost.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net8.0 - enable - enable - - $(NoWarn);CS8002 - - - - - - - - - diff --git a/dotnet/samples/dev-team/DevTeam.AgentHost/Program.cs b/dotnet/samples/dev-team/DevTeam.AgentHost/Program.cs deleted file mode 100644 index 82a2bf22ce98..000000000000 --- a/dotnet/samples/dev-team/DevTeam.AgentHost/Program.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Program.cs -using Microsoft.AutoGen.Runtime.Grpc; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); -builder.AddAgentService(); - -var app = builder.Build(); - -app.MapDefaultEndpoints(); -app.MapAgentService(); - -app.Run(); diff --git a/dotnet/samples/dev-team/DevTeam.AgentHost/appsettings.Development.json b/dotnet/samples/dev-team/DevTeam.AgentHost/appsettings.Development.json deleted file mode 100644 index 0c208ae9181e..000000000000 --- a/dotnet/samples/dev-team/DevTeam.AgentHost/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/dotnet/samples/dev-team/DevTeam.Agents/DevTeam.Agents.csproj b/dotnet/samples/dev-team/DevTeam.Agents/DevTeam.Agents.csproj deleted file mode 100644 index bc70545810bc..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Agents/DevTeam.Agents.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - diff --git a/dotnet/samples/dev-team/DevTeam.Agents/Developer/Developer.cs b/dotnet/samples/dev-team/DevTeam.Agents/Developer/Developer.cs deleted file mode 100644 index ffc474a93124..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Agents/Developer/Developer.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Developer.cs - -using DevTeam.Shared; -using Microsoft.AutoGen.Agents; -using Microsoft.AutoGen.Contracts; -using Microsoft.AutoGen.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Memory; - -namespace DevTeam.Agents; - -[TopicSubscription("devteam")] -public class Dev(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) - : SKAiAgent(worker, memory, kernel, typeRegistry), IDevelopApps, - IHandle, - IHandle -{ - public async Task Handle(CodeGenerationRequested item) - { - var code = await GenerateCode(item.Ask); - var evt = new CodeGenerated - { - Org = item.Org, - Repo = item.Repo, - IssueNumber = item.IssueNumber, - Code = code - }; - await PublishMessageAsync(evt); - } - - public async Task Handle(CodeChainClosed item) - { - //TODO: Get code from state - var lastCode = ""; // _state.State.History.Last().Message - var evt = new CodeCreated - { - Code = lastCode - }; - await PublishMessageAsync(evt); - } - - public async Task GenerateCode(string ask) - { - try - { - var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; - var instruction = "Consider the following architectural guidelines:!waf!"; - var enhancedContext = await AddKnowledge(instruction, "waf", context); - return await CallFunction(DeveloperSkills.Implement, enhancedContext); - } - catch (Exception ex) - { - logger.LogError(ex, "Error generating code"); - return ""; - } - } -} - -public interface IDevelopApps -{ - public Task GenerateCode(string ask); -} diff --git a/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLead.cs b/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLead.cs deleted file mode 100644 index ffeefe7d430f..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLead.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// DeveloperLead.cs - -using DevTeam.Shared; -using Microsoft.AutoGen.Agents; -using Microsoft.AutoGen.Contracts; -using Microsoft.AutoGen.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Memory; - -namespace DevTeam.Agents; - -[TopicSubscription("devteam")] -public class DeveloperLead(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) - : SKAiAgent(worker, memory, kernel, typeRegistry), ILeadDevelopers, - IHandle, - IHandle -{ - public async Task Handle(DevPlanRequested item) - { - var plan = await CreatePlan(item.Ask); - var evt = new DevPlanGenerated - { - Org = item.Org, - Repo = item.Repo, - IssueNumber = item.IssueNumber, - Plan = plan - }; - await PublishMessageAsync(evt); - } - - public async Task Handle(DevPlanChainClosed item) - { - // TODO: Get plan from state - var lastPlan = ""; // _state.State.History.Last().Message - var evt = new DevPlanCreated - { - Plan = lastPlan - }; - await PublishMessageAsync(evt); - } - public async Task CreatePlan(string ask) - { - try - { - var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; - var instruction = "Consider the following architectural guidelines:!waf!"; - var enhancedContext = await AddKnowledge(instruction, "waf", context); - var settings = new OpenAIPromptExecutionSettings - { - ResponseFormat = "json_object", - MaxTokens = 4096, - Temperature = 0.8, - TopP = 1 - }; - return await CallFunction(DevLeadSkills.Plan, enhancedContext, settings); - } - catch (Exception ex) - { - logger.LogError(ex, "Error creating development plan"); - return ""; - } - } -} - -public interface ILeadDevelopers -{ - public Task CreatePlan(string ask); -} diff --git a/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/ProductManager.cs b/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/ProductManager.cs deleted file mode 100644 index 5306a91838e3..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/ProductManager.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// ProductManager.cs - -using DevTeam.Shared; -using Microsoft.AutoGen.Agents; -using Microsoft.AutoGen.Contracts; -using Microsoft.AutoGen.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Memory; - -namespace DevTeam.Agents; - -[TopicSubscription("devteam")] -public class ProductManager(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) - : SKAiAgent(worker, memory, kernel, typeRegistry), IManageProducts, - IHandle, - IHandle -{ - public async Task Handle(ReadmeChainClosed item) - { - // TODO: Get readme from state - var lastReadme = ""; // _state.State.History.Last().Message - var evt = new ReadmeCreated - { - Readme = lastReadme - }; - await PublishMessageAsync(evt); - } - - public async Task Handle(ReadmeRequested item) - { - var readme = await CreateReadme(item.Ask); - var evt = new ReadmeGenerated - { - Readme = readme, - Org = item.Org, - Repo = item.Repo, - IssueNumber = item.IssueNumber - }; - await PublishMessageAsync(evt); - } - - public async Task CreateReadme(string ask) - { - try - { - var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; - var instruction = "Consider the following architectural guidelines:!waf!"; - var enhancedContext = await AddKnowledge(instruction, "waf", context); - return await CallFunction(PMSkills.Readme, enhancedContext); - } - catch (Exception ex) - { - logger.LogError(ex, "Error creating readme"); - return ""; - } - } -} - -public interface IManageProducts -{ - public Task CreateReadme(string ask); -} diff --git a/dotnet/samples/dev-team/DevTeam.Agents/Program.cs b/dotnet/samples/dev-team/DevTeam.Agents/Program.cs deleted file mode 100644 index bd9e4ad24832..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Agents/Program.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Program.cs - -using DevTeam.Agents; -using Microsoft.AutoGen.Core; -using Microsoft.AutoGen.Extensions.SemanticKernel; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -builder.ConfigureSemanticKernel(); - -builder.AddAgentWorker(builder.Configuration["AGENT_HOST"]!) - .AddAgent(nameof(Dev)) - .AddAgent(nameof(ProductManager)) - .AddAgent(nameof(DeveloperLead)); - -var app = builder.Build(); - -app.MapDefaultEndpoints(); - -app.Run(); diff --git a/dotnet/samples/dev-team/DevTeam.Agents/Properties/launchSettings.json b/dotnet/samples/dev-team/DevTeam.Agents/Properties/launchSettings.json deleted file mode 100644 index 8edfece6ad8d..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Agents/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "DevTeam.Agents": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:50669;http://localhost:50671" - } - } -} \ No newline at end of file diff --git a/dotnet/samples/dev-team/DevTeam.Agents/appsettings.Development.json b/dotnet/samples/dev-team/DevTeam.Agents/appsettings.Development.json deleted file mode 100644 index 0c208ae9181e..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Agents/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/dotnet/samples/dev-team/DevTeam.AppHost/DevTeam.AppHost.csproj b/dotnet/samples/dev-team/DevTeam.AppHost/DevTeam.AppHost.csproj index 89d121b303ea..eab38e3ba71a 100644 --- a/dotnet/samples/dev-team/DevTeam.AppHost/DevTeam.AppHost.csproj +++ b/dotnet/samples/dev-team/DevTeam.AppHost/DevTeam.AppHost.csproj @@ -21,8 +21,6 @@ - - diff --git a/dotnet/samples/dev-team/DevTeam.AppHost/Program.cs b/dotnet/samples/dev-team/DevTeam.AppHost/Program.cs index 99dd61a790bc..227a35e6bcb5 100644 --- a/dotnet/samples/dev-team/DevTeam.AppHost/Program.cs +++ b/dotnet/samples/dev-team/DevTeam.AppHost/Program.cs @@ -7,22 +7,16 @@ var qdrant = builder.AddQdrant("qdrant"); -var orleans = builder.AddOrleans("orleans") - .WithDevelopmentClustering(); +var agentHost = builder.AddContainer("agent-host", "autogen-host") + .WithEnvironment("ASPNETCORE_URLS", "https://+;http://+") + .WithEnvironment("ASPNETCORE_HTTPS_PORTS", "5001") + .WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Password", "mysecurepass") + .WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Path", "/https/devcert.pfx") + .WithBindMount("./certs", "/https/", true) + .WithHttpsEndpoint(targetPort: 5001); -var agentHost = builder.AddProject("agenthost") - .WithReference(orleans); var agentHostHttps = agentHost.GetEndpoint("https"); -//TODO: pass the right variables - aca environment -// var environmentId = builder.AddParameter("environmentId"); -// var acaSessions = builder.AddBicepTemplateString( -// name: "aca-sessions", -// bicepContent: BicepTemplates.Sessions -// ) -// .WithParameter("environmentId", environmentId); -// var acaSessionsEndpoint = acaSessions.GetOutput("endpoint"); - builder.AddProject("backend") .WithEnvironment("AGENT_HOST", $"{agentHostHttps.Property(EndpointProperty.Url)}") .WithEnvironment("Qdrant__Endpoint", $"{qdrant.Resource.HttpEndpoint.Property(EndpointProperty.Url)}") @@ -33,16 +27,10 @@ .WithEnvironment("Github__AppId", builder.Configuration["Github:AppId"]) .WithEnvironment("Github__InstallationId", builder.Configuration["Github:InstallationId"]) .WithEnvironment("Github__WebhookSecret", builder.Configuration["Github:WebhookSecret"]) - .WithEnvironment("Github__AppKey", builder.Configuration["Github:AppKey"]); + .WithEnvironment("Github__AppKey", builder.Configuration["Github:AppKey"]) + .WaitFor(agentHost) + .WaitFor(qdrant); //TODO: add this to the config in backend //.WithEnvironment("", acaSessionsEndpoint); -builder.AddProject("dev-agents") - .WithEnvironment("AGENT_HOST", $"{agentHostHttps.Property(EndpointProperty.Url)}") - .WithEnvironment("Qdrant__Endpoint", $"{qdrant.Resource.HttpEndpoint.Property(EndpointProperty.Url)}") - .WithEnvironment("Qdrant__ApiKey", $"{qdrant.Resource.ApiKeyParameter.Value}") - .WithEnvironment("Qdrant__VectorSize", "1536") - .WithEnvironment("OpenAI__Key", builder.Configuration["OpenAI:Key"]) - .WithEnvironment("OpenAI__Endpoint", builder.Configuration["OpenAI:Endpoint"]); - builder.Build().Run(); diff --git a/dotnet/samples/dev-team/DevTeam.AppHost/Properties/launchSettings.json b/dotnet/samples/dev-team/DevTeam.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000000..eae31b662c3d --- /dev/null +++ b/dotnet/samples/dev-team/DevTeam.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17034;http://localhost:15043", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21249", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22030" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15043", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19105", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20096" + } + } + } + } \ No newline at end of file diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Agents/AzureGenie.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/AzureGenie.cs index 85d498bcc5aa..59ac34ba45a3 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Agents/AzureGenie.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/AzureGenie.cs @@ -1,21 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // AzureGenie.cs -using DevTeam.Backend; -using DevTeam.Shared; -using Microsoft.AutoGen.Agents; +using DevTeam.Backend.Services; using Microsoft.AutoGen.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Memory; -namespace Microsoft.AI.DevTeam; +namespace DevTeam.Backend.Agents; -public class AzureGenie(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, IManageAzure azureService) - : SKAiAgent(worker, memory, kernel, typeRegistry), +[TopicSubscription(Consts.TopicName)] +public class AzureGenie([FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, IManageAzure azureService) + : Agent(typeRegistry), IHandle, IHandle - { - public async Task Handle(ReadmeCreated item) + public async Task Handle(ReadmeCreated item, CancellationToken cancellationToken = default) { // TODO: Not sure we need to store the files if we use ACA Sessions // //var data = item.ToData(); @@ -30,7 +26,7 @@ public async Task Handle(ReadmeCreated item) await Task.CompletedTask; } - public async Task Handle(CodeCreated item) + public async Task Handle(CodeCreated item, CancellationToken cancellationToken = default) { // TODO: Not sure we need to store the files if we use ACA Sessions // //var data = item.ToData(); diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Agents/Developer/Developer.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Developer/Developer.cs new file mode 100644 index 000000000000..b0f52f5bd5eb --- /dev/null +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Developer/Developer.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Developer.cs + +using DevTeam.Agents; +using Microsoft.AutoGen.Core; + +namespace DevTeam.Backend.Agents.Developer; + +[TopicSubscription(Consts.TopicName)] +public class Dev([FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, ILogger logger) + : AiAgent(typeRegistry, logger), IDevelopApps, + IHandle, + IHandle +{ + public async Task Handle(CodeGenerationRequested item, CancellationToken cancellationToken = default) + { + var code = await GenerateCode(item.Ask); + var evt = new CodeGenerated + { + Org = item.Org, + Repo = item.Repo, + IssueNumber = item.IssueNumber, + Code = code + }; + // TODO: Read the Topic from the agent metadata + await PublishMessageAsync(evt, topic: Consts.TopicName).ConfigureAwait(false); + } + + public async Task Handle(CodeChainClosed item, CancellationToken cancellationToken = default) + { + //TODO: Get code from state + var lastCode = ""; // _state.State.History.Last().Message + var evt = new CodeCreated + { + Code = lastCode + }; + await PublishMessageAsync(evt, topic: Consts.TopicName).ConfigureAwait(false); + } + + public async Task GenerateCode(string ask) + { + try + { + //var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; + //var instruction = "Consider the following architectural guidelines:!waf!"; + //var enhancedContext = await AddKnowledge(instruction, "waf"); + return await CallFunction(DeveloperSkills.Implement); + } + catch (Exception ex) + { + logger.LogError(ex, "Error generating code"); + return ""; + } + } +} + +public interface IDevelopApps +{ + public Task GenerateCode(string ask); +} diff --git a/dotnet/samples/dev-team/DevTeam.Agents/Developer/DeveloperPrompts.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Developer/DeveloperPrompts.cs similarity index 98% rename from dotnet/samples/dev-team/DevTeam.Agents/Developer/DeveloperPrompts.cs rename to dotnet/samples/dev-team/DevTeam.Backend/Agents/Developer/DeveloperPrompts.cs index d4b5a4f942d3..9b248807b656 100644 --- a/dotnet/samples/dev-team/DevTeam.Agents/Developer/DeveloperPrompts.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Developer/DeveloperPrompts.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // DeveloperPrompts.cs -namespace DevTeam.Agents; +namespace DevTeam.Backend.Agents.Developer; public static class DeveloperSkills { public const string Implement = """ diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Agents/DeveloperLead/DeveloperLead.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/DeveloperLead/DeveloperLead.cs new file mode 100644 index 000000000000..b935d31b8f71 --- /dev/null +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/DeveloperLead/DeveloperLead.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DeveloperLead.cs + +using DevTeam.Agents; +using Microsoft.AutoGen.Core; + +namespace DevTeam.Backend.Agents.DeveloperLead; + +[TopicSubscription(Consts.TopicName)] +public class DeveloperLead([FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, ILogger logger) + : AiAgent(typeRegistry, logger), ILeadDevelopers, + IHandle, + IHandle +{ + public async Task Handle(DevPlanRequested item, CancellationToken cancellationToken = default) + { + var plan = await CreatePlan(item.Ask); + var evt = new DevPlanGenerated + { + Org = item.Org, + Repo = item.Repo, + IssueNumber = item.IssueNumber, + Plan = plan + }; + await PublishMessageAsync(evt, topic: Consts.TopicName).ConfigureAwait(false); + } + + public async Task Handle(DevPlanChainClosed item, CancellationToken cancellationToken = default) + { + // TODO: Get plan from state + var lastPlan = ""; // _state.State.History.Last().Message + var evt = new DevPlanCreated + { + Plan = lastPlan + }; + await PublishMessageAsync(evt, topic: Consts.TopicName).ConfigureAwait(false); + } + public async Task CreatePlan(string ask) + { + try + { + //var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; + //var instruction = "Consider the following architectural guidelines:!waf!"; + //var enhancedContext = await AddKnowledge(instruction, "waf", context); + //var settings = new OpenAIPromptExecutionSettings + //{ + // ResponseFormat = "json_object", + // MaxTokens = 4096, + // Temperature = 0.8, + // TopP = 1 + //}; + return await CallFunction(DevLeadSkills.Plan); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating development plan"); + return ""; + } + } +} + +public interface ILeadDevelopers +{ + public Task CreatePlan(string ask); +} diff --git a/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLeadPrompts.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/DeveloperLead/DeveloperLeadPrompts.cs similarity index 98% rename from dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLeadPrompts.cs rename to dotnet/samples/dev-team/DevTeam.Backend/Agents/DeveloperLead/DeveloperLeadPrompts.cs index 0aeb3b26dbb4..756052fdf4f5 100644 --- a/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLeadPrompts.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/DeveloperLead/DeveloperLeadPrompts.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // DeveloperLeadPrompts.cs -namespace DevTeam.Agents; +namespace DevTeam.Backend.Agents.DeveloperLead; public static class DevLeadSkills { public const string Plan = """ diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Agents/Hubber.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Hubber.cs index 3ba0eeb69b25..8ba4ddf923ce 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Agents/Hubber.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Hubber.cs @@ -2,18 +2,14 @@ // Hubber.cs using System.Text.Json; -using DevTeam; -using DevTeam.Backend; -using DevTeam.Shared; -using Microsoft.AutoGen.Agents; +using DevTeam.Backend.Services; using Microsoft.AutoGen.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Memory; -namespace Microsoft.AI.DevTeam; +namespace DevTeam.Backend.Agents; -public class Hubber(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, IManageGithub ghService) - : SKAiAgent(worker, memory, kernel, typeRegistry), +[TopicSubscription(Consts.TopicName)] +public class Hubber([FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, IManageGithub ghService) + : Agent(typeRegistry), IHandle, IHandle, IHandle, @@ -21,7 +17,7 @@ public class Hubber(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memo IHandle, IHandle { - public async Task Handle(NewAsk item) + public async Task Handle(NewAsk item, CancellationToken cancellationToken = default) { var pmIssue = await CreateIssue(item.Org, item.Repo, item.Ask, "PM.Readme", item.IssueNumber); var devLeadIssue = await CreateIssue(item.Org, item.Repo, item.Ask, "DevLead.Plan", item.IssueNumber); @@ -30,25 +26,25 @@ public async Task Handle(NewAsk item) await CreateBranch(item.Org, item.Repo, $"sk-{item.IssueNumber}"); } - public async Task Handle(ReadmeGenerated item) + public async Task Handle(ReadmeGenerated item, CancellationToken cancellationToken = default) { var contents = string.IsNullOrEmpty(item.Readme) ? "Sorry, I got tired, can you try again please? " : item.Readme; await PostComment(item.Org, item.Repo, item.IssueNumber, contents); } - public async Task Handle(DevPlanGenerated item) + public async Task Handle(DevPlanGenerated item, CancellationToken cancellationToken = default) { var contents = string.IsNullOrEmpty(item.Plan) ? "Sorry, I got tired, can you try again please? " : item.Plan; await PostComment(item.Org, item.Repo, item.IssueNumber, contents); } - public async Task Handle(CodeGenerated item) + public async Task Handle(CodeGenerated item, CancellationToken cancellationToken = default) { var contents = string.IsNullOrEmpty(item.Code) ? "Sorry, I got tired, can you try again please? " : item.Code; await PostComment(item.Org, item.Repo, item.IssueNumber, contents); } - public async Task Handle(DevPlanCreated item) + public async Task Handle(DevPlanCreated item, CancellationToken cancellationToken = default) { var plan = JsonSerializer.Deserialize(item.Plan); var prompts = plan!.Steps.SelectMany(s => s.Subtasks!.Select(st => st.Prompt)); @@ -62,7 +58,7 @@ public async Task Handle(DevPlanCreated item) } } - public async Task Handle(ReadmeStored item) + public async Task Handle(ReadmeStored item, CancellationToken cancellationToken = default) { var branch = $"sk-{item.ParentNumber}"; await CommitToBranch(item.Org, item.Repo, item.ParentNumber, item.IssueNumber, "output", branch); diff --git a/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/PMPrompts.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/ProductManager/PMPrompts.cs similarity index 97% rename from dotnet/samples/dev-team/DevTeam.Agents/ProductManager/PMPrompts.cs rename to dotnet/samples/dev-team/DevTeam.Backend/Agents/ProductManager/PMPrompts.cs index 08d173b1166e..b10092fb046c 100644 --- a/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/PMPrompts.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/ProductManager/PMPrompts.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // PMPrompts.cs -namespace DevTeam.Agents; +namespace DevTeam.Backend.Agents.ProductManager; public static class PMSkills { public const string BootstrapProject = """ diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Agents/ProductManager/ProductManager.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/ProductManager/ProductManager.cs new file mode 100644 index 000000000000..93da47e53cdd --- /dev/null +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/ProductManager/ProductManager.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ProductManager.cs + +using DevTeam.Agents; +using Microsoft.AutoGen.Core; + +namespace DevTeam.Backend.Agents.ProductManager; + +[TopicSubscription(Consts.TopicName)] +public class ProductManager([FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, ILogger logger) + : AiAgent(typeRegistry, logger), IManageProducts, + IHandle, + IHandle +{ + public async Task Handle(ReadmeChainClosed item, CancellationToken cancellationToken = default) + { + // TODO: Get readme from state + var lastReadme = ""; // _state.State.History.Last().Message + var evt = new ReadmeCreated + { + Readme = lastReadme + }; + await PublishMessageAsync(evt, topic: Consts.TopicName).ConfigureAwait(false); + } + + public async Task Handle(ReadmeRequested item, CancellationToken cancellationToken = default) + { + var readme = await CreateReadme(item.Ask); + var evt = new ReadmeGenerated + { + Readme = readme, + Org = item.Org, + Repo = item.Repo, + IssueNumber = item.IssueNumber + }; + await PublishMessageAsync(evt, topic: Consts.TopicName).ConfigureAwait(false); + } + + public async Task CreateReadme(string ask) + { + try + { + //var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; + //var instruction = "Consider the following architectural guidelines:!waf!"; + //var enhancedContext = await AddKnowledge(instruction, "waf", context); + return await CallFunction(PMSkills.Readme); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating readme"); + return ""; + } + } +} + +public interface IManageProducts +{ + public Task CreateReadme(string ask); +} diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Agents/Sandbox.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Sandbox.cs index 19b0db00553a..306ebc945a49 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Agents/Sandbox.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Sandbox.cs @@ -3,7 +3,7 @@ // namespace DevTeam.Backend; -// public sealed class Sandbox : Agent +// public sealed class Sandbox : AgentBase // { // private const string ReminderName = "SandboxRunReminder"; // private readonly IManageAzure _azService; diff --git a/dotnet/samples/dev-team/DevTeam.Backend/AiAgent.cs b/dotnet/samples/dev-team/DevTeam.Backend/AiAgent.cs new file mode 100644 index 000000000000..427e7bb95f32 --- /dev/null +++ b/dotnet/samples/dev-team/DevTeam.Backend/AiAgent.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AiAgent.cs + +using Microsoft.AutoGen.Core; + +namespace DevTeam.Agents; + +public class AiAgent : Agent +{ + public AiAgent(AgentsMetadata eventTypes, ILogger> logger) : base(eventTypes, logger) + { + } + + protected async Task AddKnowledge(string instruction, string v) + { + throw new NotImplementedException(); + } + + protected async Task CallFunction(string prompt) + { + throw new NotImplementedException(); + } +} diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Consts.cs b/dotnet/samples/dev-team/DevTeam.Backend/Consts.cs new file mode 100644 index 000000000000..c29f662cdfd7 --- /dev/null +++ b/dotnet/samples/dev-team/DevTeam.Backend/Consts.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Consts.cs + +namespace DevTeam.Backend; + +public class Consts +{ + public const string TopicName = "devteam"; +} diff --git a/dotnet/samples/dev-team/DevTeam.Backend/DevTeam.Backend.csproj b/dotnet/samples/dev-team/DevTeam.Backend/DevTeam.Backend.csproj index f13c0cbe4f0d..34030a8817fb 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/DevTeam.Backend.csproj +++ b/dotnet/samples/dev-team/DevTeam.Backend/DevTeam.Backend.csproj @@ -1,8 +1,4 @@ - - - - - + net8.0 @@ -12,11 +8,9 @@ + - - - @@ -26,13 +20,19 @@ + + + + + + + + - - - - + + diff --git a/dotnet/samples/dev-team/DevTeam.Shared/Models/DevPlan.cs b/dotnet/samples/dev-team/DevTeam.Backend/Models/DevPlan.cs similarity index 100% rename from dotnet/samples/dev-team/DevTeam.Shared/Models/DevPlan.cs rename to dotnet/samples/dev-team/DevTeam.Backend/Models/DevPlan.cs diff --git a/dotnet/samples/dev-team/DevTeam.Shared/Options/AzureOptions.cs b/dotnet/samples/dev-team/DevTeam.Backend/Options/AzureOptions.cs similarity index 100% rename from dotnet/samples/dev-team/DevTeam.Shared/Options/AzureOptions.cs rename to dotnet/samples/dev-team/DevTeam.Backend/Options/AzureOptions.cs diff --git a/dotnet/samples/dev-team/DevTeam.Shared/Options/GithubOptions.cs b/dotnet/samples/dev-team/DevTeam.Backend/Options/GithubOptions.cs similarity index 100% rename from dotnet/samples/dev-team/DevTeam.Shared/Options/GithubOptions.cs rename to dotnet/samples/dev-team/DevTeam.Backend/Options/GithubOptions.cs diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Program.cs b/dotnet/samples/dev-team/DevTeam.Backend/Program.cs index abc37cf12608..ee75f056c1cf 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Program.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Program.cs @@ -2,11 +2,14 @@ // Program.cs using Azure.Identity; -using DevTeam.Backend; +using DevTeam.Backend.Agents; +using DevTeam.Backend.Agents.Developer; +using DevTeam.Backend.Agents.DeveloperLead; +using DevTeam.Backend.Agents.ProductManager; +using DevTeam.Backend.Services; using DevTeam.Options; -using Microsoft.AI.DevTeam; using Microsoft.AutoGen.Core; -using Microsoft.AutoGen.Extensions.SemanticKernel; +using Microsoft.AutoGen.Core.Grpc; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Options; using Octokit.Webhooks; @@ -15,16 +18,19 @@ var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -builder.ConfigureSemanticKernel(); builder.Services.AddHttpClient(); builder.Services.AddControllers(); builder.Services.AddSwaggerGen(); -builder.AddAgentWorker(builder.Configuration["AGENT_HOST"]!) +builder.AddGrpcAgentWorker(builder.Configuration["AGENT_HOST"]!) + .AddAgentWorker() .AddAgent(nameof(AzureGenie)) //.AddAgent(nameof(Sandbox)) - .AddAgent(nameof(Hubber)); + .AddAgent(nameof(Hubber)) + .AddAgent(nameof(Dev)) + .AddAgent(nameof(ProductManager)) + .AddAgent(nameof(DeveloperLead)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -58,7 +64,7 @@ var app = builder.Build(); -app.MapDefaultEndpoints(); +Microsoft.Extensions.Hosting.AspireHostingExtensions.MapDefaultEndpoints(app); app.UseRouting() .UseEndpoints(endpoints => { diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Services/AzureService.cs b/dotnet/samples/dev-team/DevTeam.Backend/Services/AzureService.cs index 3c3bbf07a0b7..619da62d6873 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Services/AzureService.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Services/AzureService.cs @@ -12,7 +12,7 @@ using DevTeam.Options; using Microsoft.Extensions.Options; -namespace DevTeam.Backend; +namespace DevTeam.Backend.Services; public class AzureService : IManageAzure { diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubAuthService.cs b/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubAuthService.cs index d1a3bb7c08df..ba6b9564b9b5 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubAuthService.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubAuthService.cs @@ -9,7 +9,7 @@ using Microsoft.IdentityModel.Tokens; using Octokit; -namespace DevTeam.Backend; +namespace DevTeam.Backend.Services; public class GithubAuthService { private readonly GithubOptions _githubSettings; diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubService.cs b/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubService.cs index 5c6dc2125fa9..1108d42e4017 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubService.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubService.cs @@ -8,7 +8,7 @@ using Octokit; using Octokit.Helpers; -namespace DevTeam.Backend; +namespace DevTeam.Backend.Services; public class GithubService : IManageGithub { diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubWebHookProcessor.cs b/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubWebHookProcessor.cs index 80660328ecae..b3d0b1aa2f5c 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubWebHookProcessor.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubWebHookProcessor.cs @@ -2,7 +2,7 @@ // GithubWebHookProcessor.cs using System.Globalization; -using DevTeam.Shared; +using Google.Protobuf; using Microsoft.AutoGen.Contracts; using Microsoft.AutoGen.Core; using Octokit.Webhooks; @@ -11,12 +11,12 @@ using Octokit.Webhooks.Events.Issues; using Octokit.Webhooks.Models; -namespace DevTeam.Backend; +namespace DevTeam.Backend.Services; -public sealed class GithubWebHookProcessor(ILogger logger, AgentWorker client) : WebhookEventProcessor +public sealed class GithubWebHookProcessor(ILogger logger, Client client) : WebhookEventProcessor { private readonly ILogger _logger = logger; - private readonly AgentWorker _client = client; + private readonly Client _client = client; protected override async Task ProcessIssuesWebhookAsync(WebhookHeaders headers, IssuesEvent issuesEvent, IssuesAction action) { @@ -43,7 +43,7 @@ protected override async Task ProcessIssuesWebhookAsync(WebhookHeaders headers, return; } - long? parentNumber = labels.TryGetValue("Parent", out string? value) ? long.Parse(value) : null; + long? parentNumber = labels.TryGetValue("Parent", out var value) ? long.Parse(value) : null; var skillName = labels.Keys.Where(k => k != "Parent").FirstOrDefault(); if (skillName == null) @@ -114,15 +114,15 @@ private async Task HandleClosingIssue(long issueNumber, string skillName, string { var subject = suffix + issueNumber.ToString(); - var evt = (skillName, functionName) switch + IMessage evt = (skillName, functionName) switch { - ("PM", "Readme") => new ReadmeChainClosed { }.ToCloudEvent(subject), - ("DevLead", "Plan") => new DevPlanChainClosed { }.ToCloudEvent(subject), - ("Developer", "Implement") => new CodeChainClosed { }.ToCloudEvent(subject), + ("PM", "Readme") => new ReadmeChainClosed { }, + ("DevLead", "Plan") => new DevPlanChainClosed { }, + ("Developer", "Implement") => new CodeChainClosed { }, _ => new CloudEvent() // TODO: default event }; - await _client.PublishEventAsync(evt); + await _client.PublishMessageAsync(evt, Consts.TopicName, subject); } private async Task HandleNewAsk(long issueNumber, string skillName, string functionName, string suffix, string input, string org, string repo) @@ -132,15 +132,15 @@ private async Task HandleNewAsk(long issueNumber, string skillName, string funct _logger.LogInformation("Handling new ask"); var subject = suffix + issueNumber.ToString(); - var evt = (skillName, functionName) switch + IMessage evt = (skillName, functionName) switch { - ("Do", "It") => new NewAsk { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo }.ToCloudEvent(subject), - ("PM", "Readme") => new ReadmeRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo }.ToCloudEvent(subject), - ("DevLead", "Plan") => new DevPlanRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo }.ToCloudEvent(subject), - ("Developer", "Implement") => new CodeGenerationRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo }.ToCloudEvent(subject), + ("Do", "It") => new NewAsk { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo }, + ("PM", "Readme") => new ReadmeRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo }, + ("DevLead", "Plan") => new DevPlanRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo }, + ("Developer", "Implement") => new CodeGenerationRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo }, _ => new CloudEvent() }; - await _client.PublishEventAsync(evt); + await _client.PublishMessageAsync(evt, Consts.TopicName, subject); } catch (Exception ex) { diff --git a/dotnet/samples/dev-team/DevTeam.ServiceDefaults/DevTeam.ServiceDefaults.csproj b/dotnet/samples/dev-team/DevTeam.ServiceDefaults/DevTeam.ServiceDefaults.csproj new file mode 100644 index 000000000000..2388aea655b8 --- /dev/null +++ b/dotnet/samples/dev-team/DevTeam.ServiceDefaults/DevTeam.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/dotnet/samples/dev-team/DevTeam.ServiceDefaults/Extensions.cs b/dotnet/samples/dev-team/DevTeam.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000000..adb2952115ff --- /dev/null +++ b/dotnet/samples/dev-team/DevTeam.ServiceDefaults/Extensions.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Extensions.cs + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/dotnet/samples/dev-team/DevTeam.Shared/DevTeam.Shared.csproj b/dotnet/samples/dev-team/DevTeam.Shared/DevTeam.Shared.csproj deleted file mode 100644 index 674bba3b13ec..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Shared/DevTeam.Shared.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - net8.0 - enable - enable - - - - - - - - - - - - - - - - - diff --git a/dotnet/samples/dev-team/DevTeam.Shared/EventExtensions.cs b/dotnet/samples/dev-team/DevTeam.Shared/EventExtensions.cs deleted file mode 100644 index bbf51dcdea6f..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Shared/EventExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// EventExtensions.cs - -using System.Globalization; -using Microsoft.AutoGen.Contracts; - -namespace DevTeam; - -public static class EventExtensions -{ - public static GithubContext ToGithubContext(this Event evt) - { - ArgumentNullException.ThrowIfNull(evt); - var data = new Dictionary();// JsonSerializer.Deserialize>(evt.Data); - return new GithubContext - { - Org = data?["org"] ?? "", - Repo = data?["repo"] ?? "", - IssueNumber = data?.TryParseLong("issueNumber") ?? default, - ParentNumber = data?.TryParseLong("parentNumber") - }; - } - - public static Dictionary ToData(this Event evt) - { - ArgumentNullException.ThrowIfNull(evt); - return //JsonSerializer.Deserialize>(evt.Data) ?? - new Dictionary(); - } - public static Dictionary ToData(this GithubContext context) - { - ArgumentNullException.ThrowIfNull(context); - - return new Dictionary { - { "org", context.Org }, - { "repo", context.Repo }, - { "issueNumber", $"{context.IssueNumber}" }, - { "parentNumber", context.ParentNumber?.ToString(CultureInfo.InvariantCulture) ?? "" } - }; - } -} - -public class GithubContext -{ - public required string Org { get; set; } - public required string Repo { get; set; } - public long IssueNumber { get; set; } - public long? ParentNumber { get; set; } - - public string Subject => $"{Org}/{Repo}/{IssueNumber}"; -} diff --git a/dotnet/samples/dev-team/DevTeam.Shared/ParseExtensions.cs b/dotnet/samples/dev-team/DevTeam.Shared/ParseExtensions.cs deleted file mode 100644 index c4681513dd10..000000000000 --- a/dotnet/samples/dev-team/DevTeam.Shared/ParseExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// ParseExtensions.cs - -namespace DevTeam; - -public static class ParseExtensions -{ - public static long TryParseLong(this Dictionary data, string key) - { - ArgumentNullException.ThrowIfNull(data); - - if (data.TryGetValue(key, out string? value) && !string.IsNullOrEmpty(value) && long.TryParse(value, out var result)) - { - return result; - } - return default; - } -} diff --git a/dotnet/samples/dev-team/Protos/messages.proto b/dotnet/samples/dev-team/Protos/messages.proto index 23db04a439f1..05861668b966 100644 --- a/dotnet/samples/dev-team/Protos/messages.proto +++ b/dotnet/samples/dev-team/Protos/messages.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package devteam; -option csharp_namespace = "DevTeam.Shared"; +option csharp_namespace = "DevTeam"; message NewAsk { string org = 1; diff --git a/dotnet/samples/dev-team/Protos/states.proto b/dotnet/samples/dev-team/Protos/states.proto index 4aaec1090639..b093aa1ad2ad 100644 --- a/dotnet/samples/dev-team/Protos/states.proto +++ b/dotnet/samples/dev-team/Protos/states.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package devteam; -option csharp_namespace = "DevTeam.Shared"; +option csharp_namespace = "DevTeam"; message DeveloperState { diff --git a/dotnet/samples/dev-team/dev team.sln b/dotnet/samples/dev-team/dev team.sln deleted file mode 100644 index f8a7aeacd924..000000000000 --- a/dotnet/samples/dev-team/dev team.sln +++ /dev/null @@ -1,49 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35327.3 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.AgentHost", "DevTeam.AgentHost\DevTeam.AgentHost.csproj", "{A6FC8B01-A177-4690-BD16-73EE3D0C06A0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.Backend", "DevTeam.Backend\DevTeam.Backend.csproj", "{2D4BAD10-85F3-4E4B-B759-13449A212A96}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.Agents", "DevTeam.Agents\DevTeam.Agents.csproj", "{A51CE540-72B0-4271-B63D-A30CAB61C227}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.AppHost", "DevTeam.AppHost\DevTeam.AppHost.csproj", "{2B8A3C64-9F4E-4CC5-9466-AFFD8E676D2E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevTeam.Shared", "DevTeam.Shared\DevTeam.Shared.csproj", "{557701A5-35D8-4CE3-BA75-D5412B4227F5}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A6FC8B01-A177-4690-BD16-73EE3D0C06A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A6FC8B01-A177-4690-BD16-73EE3D0C06A0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A6FC8B01-A177-4690-BD16-73EE3D0C06A0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A6FC8B01-A177-4690-BD16-73EE3D0C06A0}.Release|Any CPU.Build.0 = Release|Any CPU - {2D4BAD10-85F3-4E4B-B759-13449A212A96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D4BAD10-85F3-4E4B-B759-13449A212A96}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D4BAD10-85F3-4E4B-B759-13449A212A96}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D4BAD10-85F3-4E4B-B759-13449A212A96}.Release|Any CPU.Build.0 = Release|Any CPU - {A51CE540-72B0-4271-B63D-A30CAB61C227}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A51CE540-72B0-4271-B63D-A30CAB61C227}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A51CE540-72B0-4271-B63D-A30CAB61C227}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A51CE540-72B0-4271-B63D-A30CAB61C227}.Release|Any CPU.Build.0 = Release|Any CPU - {2B8A3C64-9F4E-4CC5-9466-AFFD8E676D2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B8A3C64-9F4E-4CC5-9466-AFFD8E676D2E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B8A3C64-9F4E-4CC5-9466-AFFD8E676D2E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B8A3C64-9F4E-4CC5-9466-AFFD8E676D2E}.Release|Any CPU.Build.0 = Release|Any CPU - {557701A5-35D8-4CE3-BA75-D5412B4227F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {557701A5-35D8-4CE3-BA75-D5412B4227F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {557701A5-35D8-4CE3-BA75-D5412B4227F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {557701A5-35D8-4CE3-BA75-D5412B4227F5}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {DE04DB59-B8CD-4305-875B-E71442345CCF} - EndGlobalSection -EndGlobal diff --git a/dotnet/src/Microsoft.AutoGen/AgentHost/Properties/launchSettings.json b/dotnet/src/Microsoft.AutoGen/AgentHost/Properties/launchSettings.json index cfddee319d65..796802878eb5 100644 --- a/dotnet/src/Microsoft.AutoGen/AgentHost/Properties/launchSettings.json +++ b/dotnet/src/Microsoft.AutoGen/AgentHost/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:50670;http://localhost:50673", + "applicationUrl": "https://localhost:53071;http://localhost:50673", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/dotnet/src/Microsoft.AutoGen/AgentHost/appsettings.json b/dotnet/src/Microsoft.AutoGen/AgentHost/appsettings.json index ae32fe371a70..1739187e77b2 100644 --- a/dotnet/src/Microsoft.AutoGen/AgentHost/appsettings.json +++ b/dotnet/src/Microsoft.AutoGen/AgentHost/appsettings.json @@ -3,7 +3,10 @@ "LogLevel": { "Default": "Warning", "Microsoft": "Warning", - "Microsoft.Orleans": "Warning" + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.Orleans": "Warning", + "Orleans.Runtime": "Warning" } }, "AllowedHosts": "*", diff --git a/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/InferenceAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/InferenceAgent.cs index bfcd7c6cc179..750643846fac 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/InferenceAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/InferenceAgent.cs @@ -5,10 +5,9 @@ using Microsoft.Extensions.AI; namespace Microsoft.AutoGen.Agents; public abstract class InferenceAgent( - IAgentWorker worker, - EventTypes typeRegistry, + AgentsMetadata typeRegistry, IChatClient client) - : Agent(worker, typeRegistry) + : Agent(typeRegistry) where T : IMessage, new() { protected IChatClient ChatClient { get; } = client; diff --git a/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/SKAiAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/SKAiAgent.cs index 1d0c57068e13..1c1509d263f8 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/SKAiAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/SKAiAgent.cs @@ -9,11 +9,9 @@ namespace Microsoft.AutoGen.Agents; public abstract class SKAiAgent( - IAgentWorker worker, ISemanticTextMemory memory, Kernel kernel, - EventTypes typeRegistry) : Agent( - worker, + AgentsMetadata typeRegistry) : Agent( typeRegistry) where T : class, new() { protected AgentState _state = new(); diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/ConsoleAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/ConsoleAgent.cs index 7bfe5ab8d786..2f71d2976bc1 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/ConsoleAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/ConsoleAgent.cs @@ -13,11 +13,11 @@ public abstract class ConsoleAgent : IOAgent, { // instead of the primary constructor above, make a constructr here that still calls the base constructor - public ConsoleAgent(IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : base(worker, typeRegistry) + public ConsoleAgent([FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry) : base(typeRegistry) { _route = "console"; } - public override async Task Handle(Input item) + public override async Task Handle(Input item, CancellationToken cancellationToken) { Console.WriteLine("Please enter input:"); string content = Console.ReadLine() ?? string.Empty; @@ -31,7 +31,7 @@ public override async Task Handle(Input item) await PublishMessageAsync(evt); } - public override async Task Handle(Output item) + public override async Task Handle(Output item, CancellationToken cancellationToken) { // Assuming item has a property `Content` that we want to write to the console Console.WriteLine(item.Message); diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/IHandleConsole.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/IHandleConsole.cs index 31b89453a2e6..cd0fa71eca0d 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/IHandleConsole.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/IHandleConsole.cs @@ -10,9 +10,9 @@ namespace Microsoft.AutoGen.Agents; public interface IHandleConsole : IHandle, IHandle { AgentId AgentId { get; } - ValueTask PublishMessageAsync(T message, string? source = null, CancellationToken token = default) where T : IMessage; + ValueTask PublishMessageAsync(T message, string? source = null, string? key = null, CancellationToken token = default) where T : IMessage; - async Task IHandle.Handle(Output item) + async Task IHandle.Handle(Output item, CancellationToken cancellationToken) { // Assuming item has a property `Message` that we want to write to the console Console.WriteLine(item.Message); @@ -22,9 +22,9 @@ async Task IHandle.Handle(Output item) { Route = "console" }; - await PublishMessageAsync(evt); + await PublishMessageAsync(evt).ConfigureAwait(false); } - async Task IHandle.Handle(Input item) + async Task IHandle.Handle(Input item, CancellationToken cancellationToken) { Console.WriteLine("Please enter input:"); string content = Console.ReadLine() ?? string.Empty; @@ -35,7 +35,7 @@ async Task IHandle.Handle(Input item) { Route = "console" }; - await PublishMessageAsync(evt); + await PublishMessageAsync(evt).ConfigureAwait(false); } static Task ProcessOutput(string message) { diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/FileAgent/FileAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/FileAgent/FileAgent.cs index ddab8a61ed58..a1d385286937 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/FileAgent/FileAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/FileAgent/FileAgent.cs @@ -9,16 +9,15 @@ namespace Microsoft.AutoGen.Agents; [TopicSubscription("FileIO")] public abstract class FileAgent( - IAgentWorker worker, - [FromKeyedServices("EventTypes")] EventTypes typeRegistry, + [FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, string inputPath = "input.txt", string outputPath = "output.txt" - ) : IOAgent(worker, typeRegistry), + ) : IOAgent(typeRegistry), IUseFiles, IHandle, IHandle { - public override async Task Handle(Input item) + public override async Task Handle(Input item, CancellationToken cancellationToken = default) { // validate that the file exists if (!File.Exists(inputPath)) @@ -46,7 +45,7 @@ public override async Task Handle(Input item) }; await PublishMessageAsync(evt); } - public override async Task Handle(Output item) + public override async Task Handle(Output item, CancellationToken cancellationToken = default) { using (var writer = new StreamWriter(outputPath, append: true)) { diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/IOAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/IOAgent.cs index e2b53d694885..6f2ab5a76dba 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/IOAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/IOAgent.cs @@ -5,11 +5,11 @@ using Microsoft.AutoGen.Core; namespace Microsoft.AutoGen.Agents; -public abstract class IOAgent(IAgentWorker worker, EventTypes eventTypes) : Agent(worker, eventTypes) +public abstract class IOAgent(AgentsMetadata eventTypes) : Agent(eventTypes) { public string _route = "base"; - public virtual async Task Handle(Input item) + public virtual async Task Handle(Input item, CancellationToken cancellationToken) { var evt = new InputProcessed @@ -19,7 +19,7 @@ public virtual async Task Handle(Input item) await PublishMessageAsync(evt); } - public virtual async Task Handle(Output item) + public virtual async Task Handle(Output item, CancellationToken cancellationToken) { var evt = new OutputWritten { diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/WebAPIAgent/WebAPIAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/WebAPIAgent/WebAPIAgent.cs index 3e43096bee29..a94d8d1813e6 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/WebAPIAgent/WebAPIAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/WebAPIAgent/WebAPIAgent.cs @@ -18,10 +18,9 @@ public abstract class WebAPIAgent : IOAgent, public WebAPIAgent( IAgentWorker worker, - [FromKeyedServices("EventTypes")] EventTypes typeRegistry, + [FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, ILogger logger, string url = "/agents/webio") : base( - worker, typeRegistry) { _url = url; @@ -53,7 +52,7 @@ public WebAPIAgent( app.Run(); } - public override async Task Handle(Input item) + public override async Task Handle(Input item, CancellationToken cancellationToken = default) { // Process the input (this is a placeholder, replace with actual processing logic) await ProcessInput(item.Message); @@ -65,7 +64,7 @@ public override async Task Handle(Input item) await PublishMessageAsync(evt); } - public override async Task Handle(Output item) + public override async Task Handle(Output item, CancellationToken cancellationToken = default) { // Assuming item has a property `Content` that we want to return in the response var evt = new OutputWritten diff --git a/dotnet/src/Microsoft.AutoGen/Contracts/MessageExtensions.cs b/dotnet/src/Microsoft.AutoGen/Contracts/MessageExtensions.cs deleted file mode 100644 index c531c5b76ec3..000000000000 --- a/dotnet/src/Microsoft.AutoGen/Contracts/MessageExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// MessageExtensions.cs - -using Google.Protobuf; -using Google.Protobuf.WellKnownTypes; - -namespace Microsoft.AutoGen.Contracts; - -public static class MessageExtensions -{ - private const string PROTO_DATA_CONTENT_TYPE = "application/x-protobuf"; - public static CloudEvent ToCloudEvent(this T message, string source) where T : IMessage - { - return new CloudEvent - { - ProtoData = Any.Pack(message), - Type = message.Descriptor.FullName, - Source = source, - Id = Guid.NewGuid().ToString(), - SpecVersion = "1.0", - Attributes = { { "datacontenttype", new CloudEvent.Types.CloudEventAttributeValue { CeString = PROTO_DATA_CONTENT_TYPE } } } - }; - } - public static T FromCloudEvent(this CloudEvent cloudEvent) where T : IMessage, new() - { - return cloudEvent.ProtoData.Unpack(); - } - public static AgentState ToAgentState(this T state, AgentId agentId, string eTag) where T : IMessage - { - return new AgentState - { - ProtoData = Any.Pack(state), - AgentId = agentId, - ETag = eTag - }; - } - - public static T FromAgentState(this AgentState state) where T : IMessage, new() - { - if (state.HasTextData == true) - { - if (typeof(T) == typeof(AgentState)) - { - return (T)(IMessage)state; - } - } - return state.ProtoData.Unpack(); - } -} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/App.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/App.cs index 922ef8a59500..ad4db0f38295 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/App.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/App.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Google.Protobuf; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -13,26 +12,30 @@ namespace Microsoft.AutoGen.Core.Grpc; public static class AgentsApp { // need a variable to store the runtime instance - public static WebApplication? Host { get; private set; } + public static IHost? Host { get; private set; } [MemberNotNull(nameof(Host))] - public static async ValueTask StartAsync(WebApplicationBuilder? builder = null, AgentTypes? agentTypes = null, bool local = false) + public static async ValueTask StartAsync(HostApplicationBuilder? builder = null, AgentTypes? agentTypes = null, bool local = false) { - builder ??= WebApplication.CreateBuilder(); + builder ??= new HostApplicationBuilder(); builder.Services.TryAddSingleton(DistributedContextPropagator.Current); - builder.AddAgentWorker() + if (! local) + { + builder.AddGrpcAgentWorker() + .AddAgents(agentTypes); + } else { + builder.AddAgentWorker() .AddAgents(agentTypes); - builder.AddServiceDefaults(); + } var app = builder.Build(); - app.MapDefaultEndpoints(); Host = app; await app.StartAsync().ConfigureAwait(false); return Host; } - public static async ValueTask PublishMessageAsync( + public static async ValueTask PublishMessageAsync( string topic, IMessage message, - WebApplicationBuilder? builder = null, + HostApplicationBuilder? builder = null, AgentTypes? agents = null, bool local = false) { diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorker.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorker.cs index f96f42a2c11b..398c98febc84 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorker.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorker.cs @@ -24,6 +24,7 @@ public sealed class GrpcAgentWorker( private readonly ConcurrentDictionary _agentTypes = new(); private readonly ConcurrentDictionary<(string Type, string Key), Agent> _agents = new(); private readonly ConcurrentDictionary _pendingRequests = new(); + private readonly ConcurrentDictionary> _agentsForEvent = new(); private readonly Channel<(Message Message, TaskCompletionSource WriteCompletionSource)> _outboundMessagesChannel = Channel.CreateBounded<(Message, TaskCompletionSource)>(new BoundedChannelOptions(1024) { AllowSynchronousContinuations = true, @@ -32,7 +33,7 @@ public sealed class GrpcAgentWorker( FullMode = BoundedChannelFullMode.Wait }); private readonly AgentRpc.AgentRpcClient _client = client; - private readonly IServiceProvider _serviceProvider = serviceProvider; + public readonly IServiceProvider ServiceProvider = serviceProvider; private readonly IEnumerable> _configuredAgentTypes = configuredAgentTypes; private readonly ILogger _logger = logger; private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); @@ -40,6 +41,7 @@ public sealed class GrpcAgentWorker( private Task? _readTask; private Task? _writeTask; + IServiceProvider IAgentWorker.ServiceProvider => ServiceProvider; public void Dispose() { _outboundMessagesChannel.Writer.TryComplete(); @@ -73,22 +75,30 @@ private async Task RunReadPump() message.Response.RequestId = request.OriginalRequestId; request.Agent.ReceiveMessage(message); break; - + case Message.MessageOneofCase.RegisterAgentTypeResponse: + if (!message.RegisterAgentTypeResponse.Success){ + _logger.LogError($"Failed to register agent type '{message.RegisterAgentTypeResponse.Error}'"); + } + break; + case Message.MessageOneofCase.SubscriptionResponse: + if (!message.SubscriptionResponse.Success){ + _logger.LogError($"Failed to add subscription '{message.SubscriptionResponse.Error}'"); + } + break; case Message.MessageOneofCase.CloudEvent: - - // HACK: Send the message to an instance of each agent type - // where AgentId = (namespace: event.Namespace, name: agentType) - // i.e, assume each agent type implicitly subscribes to each event. - var item = message.CloudEvent; - - foreach (var (typeName, _) in _agentTypes) + if (!_agentsForEvent.TryGetValue(item.Type, out var agents)) { - var agent = GetOrActivateAgent(new AgentId { Type = typeName, Key = item.Source }); + _logger.LogError($"This worker can't handle the event type '{item.Type}'."); + break; + } + foreach (var a in agents) + { + var agent = GetOrActivateAgent(new AgentId { Type = a.Name, Key = item.GetSubject() }); agent.ReceiveMessage(message); } - break; + default: throw new InvalidOperationException($"Unexpected message '{message}'."); } @@ -146,13 +156,18 @@ private async Task RunWritePump() { // we could not connect to the endpoint - most likely we have the wrong port or failed ssl // we need to let the user know what port we tried to connect to and then do backoff and retry - _logger.LogError(ex, "Error connecting to GRPC endpoint {Endpoint}.", channel.ToString()); + _logger.LogError(ex, "Error connecting to GRPC endpoint {Endpoint}. Check port and SSL settings.", channel.ToString()); + break; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.OK) + { + _logger.LogError(ex, "Error writing to channel, continuing (Status OK). {ex}", channel.ToString()); break; } catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) { item.WriteCompletionSource?.TrySetException(ex); - _logger.LogError(ex, "Error writing to channel."); + _logger.LogError(ex, $"Error writing to channel.{ex}"); channel = RecreateChannel(channel); continue; } @@ -175,7 +190,8 @@ private Agent GetOrActivateAgent(AgentId agentId) { if (_agentTypes.TryGetValue(agentId.Type, out var agentType)) { - agent = (Agent)ActivatorUtilities.CreateInstance(_serviceProvider, agentType, this); + agent = (Agent)ActivatorUtilities.CreateInstance(ServiceProvider, agentType, this); + Agent.Initialize(this, agent); _agents.TryAdd((agentId.Type, agentId.Key), agent); } else @@ -194,19 +210,32 @@ private async ValueTask RegisterAgentTypeAsync(string type, Type agentType, Canc var events = agentType.GetInterfaces() .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IHandle<>)) .Select(i => ReflectionHelper.GetMessageDescriptor(i.GetGenericArguments().First())?.FullName); - //var state = agentType.BaseType?.GetGenericArguments().First(); - var topicTypes = agentType.GetCustomAttributes().Select(t => t.Topic); + // add the agentType to the list of agent types that handle the event + foreach (var evt in events) + { + if (!_agentsForEvent.TryGetValue(evt!, out var agents)) + { + agents = new HashSet(); + _agentsForEvent[evt!] = agents; + } - //TODO: do something with the response (like retry on error) + agents.Add(agentType); + } + var topicTypes = agentType.GetCustomAttributes().Select(t => t.Topic); +/* var response = await _client.RegisterAgentAsync(new RegisterAgentTypeRequest + { + Type = type, + Topics = { topicTypes }, + Events = { events } + }, null, null, cancellationToken); */ await WriteChannelAsync(new Message { RegisterAgentTypeRequest = new RegisterAgentTypeRequest { - Type = type, RequestId = Guid.NewGuid().ToString(), - //TopicTypes = { topicTypes }, - //StateType = state?.Name, - //Events = { events } + Type = type, + Topics = { topicTypes }, + Events = { events } } }, cancellationToken).ConfigureAwait(false); @@ -214,7 +243,7 @@ await WriteChannelAsync(new Message { var subscriptionRequest = new Message { - AddSubscriptionRequest = new AddSubscriptionRequest + SubscriptionRequest = new SubscriptionRequest { RequestId = Guid.NewGuid().ToString(), Subscription = new Subscription @@ -227,12 +256,13 @@ await WriteChannelAsync(new Message } } }; - await WriteChannelAsync(subscriptionRequest, cancellationToken).ConfigureAwait(true); + await _client.SubscribeAsync(subscriptionRequest.SubscriptionRequest, null, null, cancellationToken); + //await WriteChannelAsync(subscriptionRequest, cancellationToken).ConfigureAwait(true); foreach (var e in events) { subscriptionRequest = new Message { - AddSubscriptionRequest = new AddSubscriptionRequest + SubscriptionRequest = new SubscriptionRequest { RequestId = Guid.NewGuid().ToString(), Subscription = new Subscription @@ -245,7 +275,8 @@ await WriteChannelAsync(new Message } } }; - await WriteChannelAsync(subscriptionRequest, cancellationToken).ConfigureAwait(true); + await _client.SubscribeAsync(subscriptionRequest.SubscriptionRequest, null, null, cancellationToken); + //await WriteChannelAsync(subscriptionRequest, cancellationToken).ConfigureAwait(true); } } } @@ -394,5 +425,24 @@ public async ValueTask ReadAsync(AgentId agentId, CancellationToken throw new KeyNotFoundException($"Failed to read AgentState for {agentId}."); } } + + public ValueTask> GetSubscriptionsAsync(Type type) + { + var agentId = new AgentId { Type = type.Name }; + var response = _client.GetSubscriptions(agentId); + return new ValueTask>([.. response.Subscriptions]); + } + + public ValueTask SubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default) + { + var response = _client.Subscribe(request, null, null, cancellationToken); + return new ValueTask(response); + } + + public ValueTask UnsubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default) + { + var response = _client.Unsubscribe(request, null, null, cancellationToken); + return new ValueTask(response); + } } diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs index 34e6276b52bb..fe8aaefa484d 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // GrpcAgentWorkerHostBuilderExtension.cs +using System.Diagnostics; using Grpc.Core; using Grpc.Net.Client.Configuration; using Microsoft.AutoGen.Contracts; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; namespace Microsoft.AutoGen.Core.Grpc; @@ -43,7 +45,23 @@ public static IHostApplicationBuilder AddGrpcAgentWorker(this IHostApplicationBu channelOptions.ThrowOperationCanceledOnCancellation = true; }); }); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + builder.Services.TryAddSingleton(DistributedContextPropagator.Current); builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => (IHostedService)sp.GetRequiredService()); + builder.Services.AddKeyedSingleton("AgentsMetadata", (sp, key) => + { + return ReflectionHelper.GetAgentsMetadata(assemblies); + }); + builder.Services.AddSingleton((s) => + { + var worker = s.GetRequiredService(); + var client = ActivatorUtilities.CreateInstance(s); + Agent.Initialize(worker, client); + return client; + }); + builder.Services.AddSingleton(new AgentApplicationBuilder(builder)); + return builder; } } diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj b/dotnet/src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj index a56f306d04b6..9ab2d7419faf 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj @@ -9,6 +9,7 @@ + diff --git a/dotnet/src/Microsoft.AutoGen/Core/Agent.cs b/dotnet/src/Microsoft.AutoGen/Core/Agent.cs index 67905ecb0543..c70f50446919 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/Agent.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/Agent.cs @@ -16,7 +16,7 @@ namespace Microsoft.AutoGen.Core; /// /// Represents the base class for an agent in the AutoGen system. /// -public abstract class Agent : IHandle +public abstract class Agent { private readonly object _lock = new(); private readonly ConcurrentDictionary> _pendingRequests = []; @@ -32,23 +32,25 @@ public abstract class Agent : IHandle public AgentId AgentId { get; private set; } private readonly Channel _mailbox = Channel.CreateUnbounded(); protected internal ILogger _logger; - public AgentMessenger Messenger { get; private set; } + public IAgentWorker Worker { get; private set; } private readonly ConcurrentDictionary _handlersByMessageType; - internal Task Completion { get; private set; } + protected readonly AgentsMetadata EventTypes; - protected readonly EventTypes EventTypes; - - protected Agent(IAgentWorker worker, - EventTypes eventTypes, + protected Agent( + AgentsMetadata eventTypes, ILogger? logger = null) { EventTypes = eventTypes; - AgentId = new AgentId(this.GetType().Name, new Guid().ToString()); + AgentId = new AgentId(this.GetType().Name, Guid.NewGuid().ToString()); _logger = logger ?? LoggerFactory.Create(builder => { }).CreateLogger(); _handlersByMessageType = new(GetType().GetHandlersLookupTable()); - Messenger = AgentMessengerFactory.Create(worker, DistributedContextPropagator.Current); - AddImplicitSubscriptionsAsync().AsTask().Wait(); - Completion = Start(); + Worker = new UninitializedAgentWorker(); + } + public static void Initialize(IAgentWorker worker, Agent agent) + { + agent.Worker = worker; + agent.Start(); + agent.AddImplicitSubscriptionsAsync().AsTask().Wait(); } private async ValueTask AddImplicitSubscriptionsAsync() @@ -61,7 +63,7 @@ private async ValueTask AddImplicitSubscriptionsAsync() foreach (var topicType in topicTypes) { - var subscriptionRequest = new AddSubscriptionRequest + var subscriptionRequest = new SubscriptionRequest { RequestId = Guid.NewGuid().ToString(), Subscription = new Subscription @@ -74,7 +76,7 @@ private async ValueTask AddImplicitSubscriptionsAsync() } }; // explicitly wait for this to complete - await Messenger.SendMessageAsync(new Message { AddSubscriptionRequest = subscriptionRequest }).ConfigureAwait(true); + Worker.SubscribeAsync(subscriptionRequest).AsTask().Wait(); } // using reflection, find all methods that Handle and subscribe to the topic T @@ -82,13 +84,15 @@ private async ValueTask AddImplicitSubscriptionsAsync() foreach (var method in handleMethods) { var eventType = method.GetParameters()[0].ParameterType; - var topic = EventTypes.EventsMap.FirstOrDefault(x => x.Value.Contains(eventType.Name)).Key; - if (topic != null) + var topics = EventTypes.GetTopicsForAgent(GetType()); + if (topics != null) { - Subscribe(nameof(topic)); + foreach (var topic in topics) + { + await SubscribeAsync(topic).ConfigureAwait(true); + } } } - } /// @@ -148,7 +152,7 @@ protected internal async Task HandleRpcMessage(Message msg, CancellationToken ca { var activity = this.ExtractActivity(msg.CloudEvent.Type, msg.CloudEvent.Attributes); await this.InvokeWithActivityAsync( - static ((Agent Agent, CloudEvent Item) state, CancellationToken _) => state.Agent.CallHandler(state.Item), + static ((Agent Agent, CloudEvent Item) state, CancellationToken ct) => state.Agent.CallHandlerAsync(state.Item, ct), (this, msg.CloudEvent), activity, msg.CloudEvent.Type, cancellationToken).ConfigureAwait(false); @@ -169,35 +173,49 @@ await this.InvokeWithActivityAsync( break; } } - public List Subscribe(string topic) + public async ValueTask> GetSubscriptionsAsync() + { + return await Worker.GetSubscriptionsAsync(GetType()).ConfigureAwait(false); + } + public async ValueTask SubscribeAsync(string topic) + { + return await UpdateSubscriptionAsync(topic, true).ConfigureAwait(true); + } + public async ValueTask UnsubscribeAsync(string topic) + { + return await UpdateSubscriptionAsync(topic, false).ConfigureAwait(true); + } + private async ValueTask UpdateSubscriptionAsync(string topic, bool subscribe) { - Message message = new() + SubscriptionRequest subscriptionRequest = new() { - AddSubscriptionRequest = new() + RequestId = Guid.NewGuid().ToString(), + Subscription = new Subscription { - RequestId = Guid.NewGuid().ToString(), - Subscription = new Subscription + TypeSubscription = new TypeSubscription { - TypeSubscription = new TypeSubscription - { - TopicType = topic, - AgentType = this.AgentId.Key - } + TopicType = topic, + AgentType = this.AgentId.Type } } }; - Messenger.SendMessageAsync(message).AsTask().Wait(); - - return new List { topic }; + var subscriptionResponse = subscribe + ? await Worker.SubscribeAsync(subscriptionRequest).ConfigureAwait(true) + : await Worker.UnsubscribeAsync(subscriptionRequest).ConfigureAwait(true); + if (!subscriptionResponse.Success) + { + _logger.LogError($"{GetType}{AgentId.Key}: Failed to unsubscribe from topic {topic}"); + } + return subscriptionResponse; } public async Task StoreAsync(AgentState state, CancellationToken cancellationToken = default) { - await Messenger.StoreAsync(state, cancellationToken).ConfigureAwait(false); + await Worker.StoreAsync(state, cancellationToken).ConfigureAwait(false); return; } public async Task ReadAsync(AgentId agentId, CancellationToken cancellationToken = default) where T : IMessage, new() { - var agentstate = await Messenger.ReadAsync(agentId, cancellationToken).ConfigureAwait(false); + var agentstate = await Worker.ReadAsync(agentId, cancellationToken).ConfigureAwait(false); return agentstate.FromAgentState(); } private void OnResponseCore(RpcResponse response) @@ -226,7 +244,9 @@ private async Task OnRequestCoreAsync(RpcRequest request, CancellationToken canc { response = new RpcResponse { Error = ex.Message }; } - await Messenger.SendResponseAsync(request, response, cancellationToken).ConfigureAwait(false); + response.RequestId = request.RequestId; + + await Worker.SendResponseAsync(response, cancellationToken).ConfigureAwait(false); } protected async Task RequestAsync(AgentId target, string method, Dictionary parameters) @@ -250,7 +270,7 @@ protected async Task RequestAsync(AgentId target, string method, Di activity?.SetTag("peer.service", target.ToString()); var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - Messenger!.Update(request, activity); + IAgentWorkerExtensions.Update(this.Worker, request, activity); await this.InvokeWithActivityAsync( static async (state, ct) => { @@ -258,7 +278,7 @@ static async (state, ct) => self._pendingRequests.AddOrUpdate(request.RequestId, _ => completion, (_, __) => completion); - await state.Item1.Messenger!.SendRequestAsync(state.Item1, state.request, ct).ConfigureAwait(false); + await state.Item1.Worker!.SendRequestAsync(state.Item1, state.request, ct).ConfigureAwait(false); await completion.Task.ConfigureAwait(false); }, @@ -269,92 +289,103 @@ static async (state, ct) => // Return the result from the already-completed task return await completion.Task.ConfigureAwait(false); } - - public async ValueTask PublishMessageAsync(T message, string? source = null, CancellationToken token = default) where T : IMessage + /// + /// Publishes a message asynchronously. + /// + /// The type of the message. + /// The message to publish. + /// The source of the message. + /// A token to cancel the operation. + /// A task representing the asynchronous operation. + public async ValueTask PublishMessageAsync(T @event, string? topic = null, string? key = null, CancellationToken token = default) where T : IMessage { - var src = string.IsNullOrWhiteSpace(source) ? this.AgentId.Key : source; - var evt = message.ToCloudEvent(src); + var k = string.IsNullOrWhiteSpace(key) ? AgentId.Key : key; + var topicType = string.IsNullOrWhiteSpace(topic) ? "default" : topic; + var evt = @event.ToCloudEvent(k, topicType); await PublishEventAsync(evt, token).ConfigureAwait(false); } - public async ValueTask PublishEventAsync(CloudEvent item, CancellationToken cancellationToken = default) { var activity = s_source.StartActivity($"PublishEventAsync '{item.Type}'", ActivityKind.Client, Activity.Current?.Context ?? default); activity?.SetTag("peer.service", $"{item.Type}/{item.Source}"); // TODO: fix activity - Messenger.Update(item, activity); + IAgentWorkerExtensions.Update(this.Worker, item, activity); await this.InvokeWithActivityAsync( static async ((Agent Agent, CloudEvent Event) state, CancellationToken ct) => { - await state.Agent.Messenger.PublishEventAsync(state.Event).ConfigureAwait(false); + await state.Agent.Worker.PublishEventAsync(state.Event).ConfigureAwait(false); }, (this, item), activity, item.Type, cancellationToken).ConfigureAwait(false); } - public Task CallHandler(CloudEvent item) + public Task CallHandlerAsync(CloudEvent item, CancellationToken cancellationToken = default) { // Only send the event to the handler if the agent type is handling that type - // foreach of the keys in the EventTypes.EventsMap[] if it contains the item.type - foreach (var key in EventTypes.EventsMap.Keys) + if (EventTypes.CheckIfTypeHandles(GetType(), eventName: item.Type)) { - if (EventTypes.EventsMap[key].Contains(item.Type)) + var payload = item.ProtoData.Unpack(EventTypes.TypeRegistry); + var eventType = EventTypes.GetEventTypeByName(item.Type); + if (eventType == null) { - var payload = item.ProtoData.Unpack(EventTypes.TypeRegistry); - var convertedPayload = Convert.ChangeType(payload, EventTypes.Types[item.Type]); - var genericInterfaceType = typeof(IHandle<>).MakeGenericType(EventTypes.Types[item.Type]); + _logger.LogError($"Event type {item.Type} not found in the registry"); + return Task.CompletedTask; + } + var convertedPayload = Convert.ChangeType(payload, eventType); + var genericInterfaceType = typeof(IHandle<>).MakeGenericType(eventType); - MethodInfo methodInfo; - try + MethodInfo methodInfo; + try + { + // check that our target actually implements this interface, otherwise call the default static + if (genericInterfaceType.IsAssignableFrom(this.GetType())) { - // check that our target actually implements this interface, otherwise call the default static - if (genericInterfaceType.IsAssignableFrom(this.GetType())) - { - methodInfo = genericInterfaceType.GetMethod(nameof(IHandle.Handle), BindingFlags.Public | BindingFlags.Instance) - ?? throw new InvalidOperationException($"Method not found on type {genericInterfaceType.FullName}"); - return methodInfo.Invoke(this, [payload]) as Task ?? Task.CompletedTask; - } - else - { - // The error here is we have registered for an event that we do not have code to listen to - throw new InvalidOperationException($"No handler found for event '{item.Type}'; expecting IHandle<{item.Type}> implementation."); - } + methodInfo = genericInterfaceType.GetMethod(nameof(IHandle.Handle), BindingFlags.Public | BindingFlags.Instance) + ?? throw new InvalidOperationException($"Method not found on type {genericInterfaceType.FullName}"); + return methodInfo.Invoke(this, new object[] { convertedPayload, cancellationToken }) as Task ?? Task.CompletedTask; } - catch (Exception ex) + else { - _logger.LogError(ex, $"Error invoking method {nameof(IHandle.Handle)}"); - throw; // TODO: ? + // The error here is we have registered for an event that we do not have code to listen to + throw new InvalidOperationException($"Agent Type '{GetType()}' is registered to handle this type but no handler found for event '{item.Type}'; expecting IHandle<{item.Type}> implementation."); } } + catch (Exception ex) + { + _logger.LogError(ex, $"Error invoking method {nameof(IHandle.Handle)}"); + throw; // TODO: ? + } } - return Task.CompletedTask; } public Task HandleRequestAsync(RpcRequest request) => Task.FromResult(new RpcResponse { Error = "Not implemented" }); - //TODO: should this be async and cancellable? - public virtual Task HandleObject(object item) + /// + /// Handles a generic object + /// + /// The object to handle + /// The cancellation token + /// A task representing the asynchronous operation. + /// TODO: this is only called from tests, should we remove it? + public virtual async Task HandleObjectAsync(object item, CancellationToken cancellationToken = default) { // get all Handle methods var handleTMethods = this.GetType().GetMethods().Where(m => m.Name == "Handle" && m.GetParameters().Length == 1).ToList(); - // get the one that matches the type of the item var handleTMethod = handleTMethods.FirstOrDefault(m => m.GetParameters()[0].ParameterType == item.GetType()); - // if we found one, invoke it if (handleTMethod != null) { - return (Task)handleTMethod.Invoke(this, [item])!; + await (Task)handleTMethod.Invoke(this, [item])!; } - // otherwise, complain - throw new InvalidOperationException($"No handler found for type {item.GetType().FullName}"); + _logger.LogError($"No handler found for type {item.GetType().FullName}"); } public async ValueTask PublishEventAsync(string topic, IMessage evt, CancellationToken cancellationToken = default) { - await PublishEventAsync(evt.ToCloudEvent(topic), cancellationToken).ConfigureAwait(false); + await PublishEventAsync(evt.ToCloudEvent(key: GetType().Name, topic: topic), cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentExtensions.cs index 911107f784c5..50d25b20ada4 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/AgentExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/AgentExtensions.cs @@ -22,7 +22,7 @@ public static class AgentExtensions public static Activity? ExtractActivity(this Agent agent, string activityName, IDictionary metadata) { Activity? activity; - var (traceParent, traceState) = agent.Messenger.GetTraceIdAndState(metadata); + (string? traceParent, string? traceState) = IAgentWorkerExtensions.GetTraceIdAndState(agent.Worker, metadata); if (!string.IsNullOrEmpty(traceParent)) { if (ActivityContext.TryParse(traceParent, traceState, isRemote: true, out var parentContext)) @@ -43,7 +43,7 @@ public static class AgentExtensions activity.TraceStateString = traceState; } - var baggage = agent.Messenger.ExtractMetadata(metadata); + var baggage = IAgentWorkerExtensions.ExtractMetadata(agent.Worker, metadata); foreach (var baggageItem in baggage) { diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentMessenger.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentMessenger.cs deleted file mode 100644 index 66b1c0c65da9..000000000000 --- a/dotnet/src/Microsoft.AutoGen/Core/AgentMessenger.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// AgentMessenger.cs - -using System.Diagnostics; -using Google.Protobuf.Collections; -using Microsoft.AutoGen.Contracts; -using static Microsoft.AutoGen.Contracts.CloudEvent.Types; - -namespace Microsoft.AutoGen.Core; - -public sealed class AgentMessenger(IAgentWorker worker, DistributedContextPropagator distributedContextPropagator) -{ - private readonly IAgentWorker worker = worker; - - private DistributedContextPropagator DistributedContextPropagator { get; } = distributedContextPropagator; - public (string?, string?) GetTraceIdAndState(IDictionary metadata) - { - DistributedContextPropagator.ExtractTraceIdAndState(metadata, - static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => - { - var metadata = (IDictionary)carrier!; - fieldValues = null; - metadata.TryGetValue(fieldName, out fieldValue); - }, - out var traceParent, - out var traceState); - return (traceParent, traceState); - } - public (string?, string?) GetTraceIdAndState(MapField metadata) - { - DistributedContextPropagator.ExtractTraceIdAndState(metadata, - static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => - { - var metadata = (MapField)carrier!; - fieldValues = null; - metadata.TryGetValue(fieldName, out var ceValue); - fieldValue = ceValue?.CeString; - }, - out var traceParent, - out var traceState); - return (traceParent, traceState); - } - public void Update(RpcRequest request, Activity? activity = null) - { - DistributedContextPropagator.Inject(activity, request.Metadata, static (carrier, key, value) => - { - var metadata = (IDictionary)carrier!; - if (metadata.TryGetValue(key, out _)) - { - metadata[key] = value; - } - else - { - metadata.Add(key, value); - } - }); - } - public void Update(CloudEvent cloudEvent, Activity? activity = null) - { - DistributedContextPropagator.Inject(activity, cloudEvent.Attributes, static (carrier, key, value) => - { - var mapField = (MapField)carrier!; - if (mapField.TryGetValue(key, out var ceValue)) - { - mapField[key] = new CloudEventAttributeValue { CeString = value }; - } - else - { - mapField.Add(key, new CloudEventAttributeValue { CeString = value }); - } - }); - } - public async ValueTask SendResponseAsync(RpcRequest request, RpcResponse response, CancellationToken cancellationToken = default) - { - response.RequestId = request.RequestId; - await worker.SendResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - public async ValueTask SendRequestAsync(Agent agent, RpcRequest request, CancellationToken cancellationToken = default) - { - await worker.SendRequestAsync(agent, request, cancellationToken).ConfigureAwait(false); - } - public async ValueTask SendMessageAsync(Message message, CancellationToken cancellationToken = default) - { - await worker.SendMessageAsync(message, cancellationToken).ConfigureAwait(false); - } - public async ValueTask PublishEventAsync(CloudEvent @event, CancellationToken cancellationToken = default) - { - await worker.PublishEventAsync(@event, cancellationToken).ConfigureAwait(false); - } - public async ValueTask StoreAsync(AgentState value, CancellationToken cancellationToken = default) - { - await worker.StoreAsync(value, cancellationToken).ConfigureAwait(false); - } - public ValueTask ReadAsync(AgentId agentId, CancellationToken cancellationToken = default) - { - return worker.ReadAsync(agentId, cancellationToken); - } - - public IDictionary ExtractMetadata(IDictionary metadata) - { - var baggage = DistributedContextPropagator.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => - { - var metadata = (IDictionary)carrier!; - fieldValues = null; - metadata.TryGetValue(fieldName, out fieldValue); - }); - - return baggage as IDictionary ?? new Dictionary(); - } - - public IDictionary ExtractMetadata(MapField metadata) - { - var baggage = DistributedContextPropagator.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => - { - var metadata = (MapField)carrier!; - fieldValues = null; - metadata.TryGetValue(fieldName, out var ceValue); - fieldValue = ceValue?.CeString; - }); - - return baggage as IDictionary ?? new Dictionary(); - } -} diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentMessengerFactory.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentMessengerFactory.cs deleted file mode 100644 index c008f9f2d5aa..000000000000 --- a/dotnet/src/Microsoft.AutoGen/Core/AgentMessengerFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// AgentMessengerFactory.cs - -using System.Diagnostics; -namespace Microsoft.AutoGen.Core; -public class AgentMessengerFactory() -{ - public static AgentMessenger Create(IAgentWorker worker, DistributedContextPropagator distributedContextPropagator) - { - return new AgentMessenger(worker, distributedContextPropagator); - } -} diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentWorker.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentWorker.cs index 3b70461a39fd..aaf6c795d14d 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/AgentWorker.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/AgentWorker.cs @@ -9,12 +9,19 @@ namespace Microsoft.AutoGen.Core; +/// +/// Represents a worker that manages agents and handles messages. +/// +/// +/// Initializes a new instance of the class. +/// +/// The application lifetime. +/// The service provider. +/// The configured agent types. public class AgentWorker( -IHostApplicationLifetime hostApplicationLifetime, -IServiceProvider serviceProvider, -[FromKeyedServices("AgentTypes")] IEnumerable> configuredAgentTypes) : - IHostedService, - IAgentWorker + IHostApplicationLifetime hostApplicationLifetime, + IServiceProvider serviceProvider, + [FromKeyedServices("AgentTypes")] IEnumerable> configuredAgentTypes) : IHostedService, IAgentWorker { private readonly ConcurrentDictionary _agentTypes = new(); private readonly ConcurrentDictionary<(string Type, string Key), Agent> _agents = new(); @@ -22,24 +29,26 @@ public class AgentWorker( private readonly ConcurrentDictionary _agentStates = new(); private readonly ConcurrentDictionary _pendingClientRequests = new(); private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); - private readonly IServiceProvider _serviceProvider = serviceProvider; + public IServiceProvider ServiceProvider { get; } = serviceProvider; private readonly IEnumerable> _configuredAgentTypes = configuredAgentTypes; - private readonly ConcurrentDictionary _subscriptionsByAgentType = new(); + private readonly ConcurrentDictionary> _subscriptionsByAgentType = new(); private readonly ConcurrentDictionary> _subscriptionsByTopic = new(); private readonly CancellationTokenSource _shutdownCancellationToken = new(); private Task? _mailboxTask; private readonly object _channelLock = new(); - // this is the in-memory version - we just pass the message directly to the agent(s) that handle this type of event + /// public async ValueTask PublishEventAsync(CloudEvent cloudEvent, CancellationToken cancellationToken = default) { foreach (var (typeName, _) in _agentTypes) { if (typeName == nameof(Client)) { continue; } - var agent = GetOrActivateAgent(new AgentId(typeName, cloudEvent.Source)); + var agent = GetOrActivateAgent(new AgentId { Type = typeName, Key = cloudEvent.GetSubject() }); agent.ReceiveMessage(new Message { CloudEvent = cloudEvent }); } } + + /// public async ValueTask SendRequestAsync(Agent agent, RpcRequest request, CancellationToken cancellationToken = default) { var requestId = Guid.NewGuid().ToString(); @@ -47,21 +56,28 @@ public async ValueTask SendRequestAsync(Agent agent, RpcRequest request, Cancell request.RequestId = requestId; await _mailbox.Writer.WriteAsync(request, cancellationToken).ConfigureAwait(false); } + + /// public ValueTask SendResponseAsync(RpcResponse response, CancellationToken cancellationToken = default) { return _mailbox.Writer.WriteAsync(new Message { Response = response }, cancellationToken); } + + /// public ValueTask SendMessageAsync(Message message, CancellationToken cancellationToken = default) { return _mailbox.Writer.WriteAsync(message, cancellationToken); } + + /// public ValueTask StoreAsync(AgentState value, CancellationToken cancellationToken = default) { var agentId = value.AgentId ?? throw new InvalidOperationException("AgentId is required when saving AgentState."); - // add or update _agentStates with the new state var response = _agentStates.AddOrUpdate(agentId.ToString(), value, (key, oldValue) => value); return ValueTask.CompletedTask; } + + /// public ValueTask ReadAsync(AgentId agentId, CancellationToken cancellationToken = default) { _agentStates.TryGetValue(agentId.ToString(), out var state); @@ -74,6 +90,10 @@ public ValueTask ReadAsync(AgentId agentId, CancellationToken cancel throw new KeyNotFoundException($"Failed to read AgentState for {agentId}."); } } + + /// + /// Runs the message pump. + /// public async Task RunMessagePump() { await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); @@ -94,10 +114,10 @@ public async Task RunMessagePump() agentToInvoke.ReceiveMessage(msg); } break; - case Message msg when msg.AddSubscriptionRequest != null: - await AddSubscriptionRequestAsync(msg.AddSubscriptionRequest).ConfigureAwait(true); + case Message msg when msg.SubscriptionRequest != null: + await SubscribeAsync(msg.SubscriptionRequest).ConfigureAwait(true); break; - case Message msg when msg.AddSubscriptionResponse != null: + case Message msg when msg.SubscriptionResponse != null: break; case Message msg when msg.RegisterAgentTypeResponse != null: break; @@ -114,22 +134,45 @@ public async Task RunMessagePump() } } } - private async ValueTask AddSubscriptionRequestAsync(AddSubscriptionRequest subscription) + public async ValueTask SubscribeAsync(SubscriptionRequest subscription, CancellationToken cancellationToken = default) { var topic = subscription.Subscription.TypeSubscription.TopicType; var agentType = subscription.Subscription.TypeSubscription.AgentType; - _subscriptionsByAgentType[agentType] = subscription.Subscription; + _subscriptionsByAgentType.GetOrAdd(key: agentType, _ => []).Add(subscription.Subscription); _subscriptionsByTopic.GetOrAdd(topic, _ => []).Add(agentType); - Message response = new() + var response = new SubscriptionResponse + { + RequestId = subscription.RequestId, + Error = "", + Success = true + }; + return response; + } + public async ValueTask UnsubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default) + { + var topic = request.Subscription.TypeSubscription.TopicType; + var agentType = request.Subscription.TypeSubscription.AgentType; + if (_subscriptionsByAgentType.TryGetValue(agentType, out var subscriptions)) + { + while (subscriptions.Remove(request.Subscription)) + { + //ensures all instances are removed + } + } + if (_subscriptionsByTopic.TryGetValue(topic, out var agentTypes)) { - AddSubscriptionResponse = new() + while (agentTypes.Remove(agentType)) { - RequestId = subscription.RequestId, - Error = "", - Success = true + //ensures all instances are removed } + } + var response = new SubscriptionResponse + { + RequestId = request.RequestId, + Error = "", + Success = true }; - await _mailbox.Writer.WriteAsync(response).ConfigureAwait(false); + return response; } public async Task StartAsync(CancellationToken cancellationToken) @@ -162,6 +205,8 @@ void StartCore() } } } + + /// public async Task StopAsync(CancellationToken cancellationToken) { _shutdownCts.Cancel(); @@ -176,14 +221,26 @@ public async Task StopAsync(CancellationToken cancellationToken) { } } + + /// + /// Gets or activates an agent. + /// + /// The agent ID. + /// The activated agent. + private Agent GetOrActivateAgent(AgentId agentId) { if (!_agents.TryGetValue((agentId.Type, agentId.Key), out var agent)) { if (_agentTypes.TryGetValue(agentId.Type, out var agentType)) { - agent = (Agent)ActivatorUtilities.CreateInstance(_serviceProvider, agentType, this); - _agents.TryAdd((agentId.Type, agentId.Key), agent); + using (var scope = ServiceProvider.CreateScope()) + { + var scopedProvider = scope.ServiceProvider; + agent = (Agent)ActivatorUtilities.CreateInstance(scopedProvider, agentType); + Agent.Initialize(this, agent); + _agents.TryAdd((agentId.Type, agentId.Key), agent); + } } else { @@ -193,4 +250,13 @@ private Agent GetOrActivateAgent(AgentId agentId) return agent; } + + public ValueTask> GetSubscriptionsAsync(Type type) + { + if (_subscriptionsByAgentType.TryGetValue(type.Name, out var subscriptions)) + { + return new ValueTask>(subscriptions); + } + return new ValueTask>([]); + } } diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentsMetadata.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentsMetadata.cs new file mode 100644 index 000000000000..b3812d2e47cc --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core/AgentsMetadata.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentsMetadata.cs + +using System.Collections.Concurrent; +using Google.Protobuf.Reflection; + +namespace Microsoft.AutoGen.Core; + +/// +/// Represents a collection of event types and their associated metadata. +/// +public sealed class AgentsMetadata +{ + /// + /// Initializes a new instance of the class. + /// + /// The type registry containing protobuf type information. + /// A dictionary mapping event names to their corresponding types. + /// A dictionary mapping types to a set of event names associated with those types. + public AgentsMetadata(TypeRegistry typeRegistry, Dictionary types, Dictionary> eventsMap, Dictionary> topicsMap) + { + TypeRegistry = typeRegistry; + _types = new(types); + _eventsMap = new(eventsMap); + _topicsMap = new(topicsMap); + } + + /// + /// Gets the type registry containing protobuf type information. + /// + public TypeRegistry TypeRegistry { get; } + + private ConcurrentDictionary _types; + + private ConcurrentDictionary> _eventsMap; + private ConcurrentDictionary> _topicsMap; + + /// + /// Checks if a given type handles a specific event name. + /// + /// The type to check. + /// The event name to check. + /// true if the type handles the event name; otherwise, false. + public bool CheckIfTypeHandles(Type type, string eventName) + { + if (_eventsMap.TryGetValue(type, out var events)) + { + return events.Contains(eventName); + } + return false; + } + + /// + /// Gets the event type by its name. + /// + /// The name of the event type. + /// The event type if found; otherwise, null. + public Type? GetEventTypeByName(string type) + { + if (_types.TryGetValue(type, out var eventType)) + { + return eventType; + } + return null; + } + + public HashSet? GetEventsForAgent(Type agent) + { + if (_eventsMap.TryGetValue(agent, out var events)) + { + return events; + } + return null; + } + + public HashSet? GetTopicsForAgent(Type agent) + { + if (_topicsMap.TryGetValue(agent, out var topics)) + { + return topics; + } + return null; + } +} + diff --git a/dotnet/src/Microsoft.AutoGen/Core/App.cs b/dotnet/src/Microsoft.AutoGen/Core/App.cs index f0850e29ed3a..e8d84789f71b 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/App.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/App.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Google.Protobuf; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -13,27 +12,25 @@ namespace Microsoft.AutoGen.Core; public static class AgentsApp { // need a variable to store the runtime instance - public static WebApplication? Host { get; private set; } + public static IHost? Host { get; private set; } [MemberNotNull(nameof(Host))] - public static async ValueTask StartAsync(WebApplicationBuilder? builder = null, AgentTypes? agentTypes = null) + public static async ValueTask StartAsync(HostApplicationBuilder? builder = null, AgentTypes? agentTypes = null) { - builder ??= WebApplication.CreateBuilder(); + builder ??= new HostApplicationBuilder(); builder.Services.TryAddSingleton(DistributedContextPropagator.Current); builder.AddAgentWorker() .AddAgents(agentTypes); - builder.AddServiceDefaults(); var app = builder.Build(); - app.MapDefaultEndpoints(); Host = app; await app.StartAsync().ConfigureAwait(false); return Host; } - public static async ValueTask PublishMessageAsync( + public static async ValueTask PublishMessageAsync( string topic, IMessage message, - WebApplicationBuilder? builder = null, + HostApplicationBuilder? builder = null, AgentTypes? agents = null, bool local = false) { diff --git a/dotnet/src/Microsoft.AutoGen/Core/Client.cs b/dotnet/src/Microsoft.AutoGen/Core/Client.cs index 1d523005fedf..132dcd998c5c 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/Client.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/Client.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AutoGen.Core; -public sealed class Client(IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes eventTypes) - : Agent(worker, eventTypes) +public sealed class Client([FromKeyedServices("AgentsMetadata")] AgentsMetadata eventTypes) + : Agent(eventTypes) { } diff --git a/dotnet/src/Microsoft.AutoGen/Core/HostBuilderExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core/HostBuilderExtensions.cs index 95d4d7ce7553..339ea6fd12b2 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/HostBuilderExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/HostBuilderExtensions.cs @@ -3,9 +3,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Google.Protobuf; -using Google.Protobuf.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -14,8 +11,6 @@ namespace Microsoft.AutoGen.Core; public static class HostBuilderExtensions { - private const string _defaultAgentServiceAddress = "https://localhost:53071"; - public static IHostApplicationBuilder AddAgent< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TAgent>(this IHostApplicationBuilder builder, string typeName) where TAgent : Agent { @@ -30,90 +25,27 @@ public static IHostApplicationBuilder AddAgent(this IHostApplicationBuilder buil return builder; } - public static IHostApplicationBuilder AddAgentWorker(this IHostApplicationBuilder builder, string? agentServiceAddress = null) + public static IHostApplicationBuilder AddAgentWorker(this IHostApplicationBuilder builder) { - agentServiceAddress ??= builder.Configuration["AGENT_HOST"] ?? _defaultAgentServiceAddress; + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); builder.Services.TryAddSingleton(DistributedContextPropagator.Current); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => (IHostedService)sp.GetRequiredService()); - builder.Services.AddKeyedSingleton("EventTypes", (sp, key) => + builder.Services.AddKeyedSingleton("AgentsMetadata", (sp, key) => { - var interfaceType = typeof(IMessage); - var pairs = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()) - .Where(type => interfaceType.IsAssignableFrom(type) && type.IsClass && !type.IsAbstract) - .Select(t => (t, GetMessageDescriptor(t))); - - var descriptors = pairs.Select(t => t.Item2); - var typeRegistry = TypeRegistry.FromMessages(descriptors); - var types = pairs.ToDictionary(item => item.Item2?.FullName ?? "", item => item.t); - - var eventsMap = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()) - .Where(type => ReflectionHelper.IsSubclassOfGeneric(type, typeof(Agent)) && !type.IsAbstract) - .Select(t => (t, t.GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IHandle<>)) - .Select(i => (GetMessageDescriptor(i.GetGenericArguments().First())?.FullName ?? "")).ToHashSet())) - .ToDictionary(item => item.t, item => item.Item2); - // if the assembly contains any interfaces of type IHandler, then add all the methods of the interface to the eventsMap - var handlersMap = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()) - .Where(type => ReflectionHelper.IsSubclassOfGeneric(type, typeof(Agent)) && !type.IsAbstract) - .Select(t => (t, t.GetMethods() - .Where(m => m.Name == "Handle") - .Select(m => (GetMessageDescriptor(m.GetParameters().First().ParameterType)?.FullName ?? "")).ToHashSet())) - .ToDictionary(item => item.t, item => item.Item2); - // get interfaces implemented by the agent and get the methods of the interface if they are named Handle - var ifaceHandlersMap = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()) - .Where(type => ReflectionHelper.IsSubclassOfGeneric(type, typeof(Agent)) && !type.IsAbstract) - .Select(t => t.GetInterfaces() - .Select(i => (t, i, i.GetMethods() - .Where(m => m.Name == "Handle") - .Select(m => (GetMessageDescriptor(m.GetParameters().First().ParameterType)?.FullName ?? "")) - //to dictionary of type t and paramter type of the method - .ToDictionary(m => m, m => m).Keys.ToHashSet())).ToList()); - // for each item in ifaceHandlersMap, add the handlers to eventsMap with item as the key - foreach (var item in ifaceHandlersMap) - { - foreach (var iface in item) - { - if (eventsMap.TryGetValue(iface.Item2, out var events)) - { - events.UnionWith(iface.Item3); - } - else - { - eventsMap[iface.Item2] = iface.Item3; - } - } - } - - // merge the handlersMap into the eventsMap - foreach (var item in handlersMap) - { - if (eventsMap.TryGetValue(item.Key, out var events)) - { - events.UnionWith(item.Value); - } - else - { - eventsMap[item.Key] = item.Value; - } - } - return new EventTypes(typeRegistry, types, eventsMap); + return ReflectionHelper.GetAgentsMetadata(assemblies); + }); + builder.Services.AddSingleton((s) => + { + var worker = s.GetRequiredService(); + var client = ActivatorUtilities.CreateInstance(s); + Agent.Initialize(worker, client); + return client; }); - builder.Services.AddSingleton(); builder.Services.AddSingleton(new AgentApplicationBuilder(builder)); return builder; } - - private static MessageDescriptor? GetMessageDescriptor(Type type) - { - var property = type.GetProperty("Descriptor", BindingFlags.Static | BindingFlags.Public); - return property?.GetValue(null) as MessageDescriptor; - } } public sealed class AgentApplicationBuilder(IHostApplicationBuilder builder) { diff --git a/dotnet/src/Microsoft.AutoGen/Core/IAgentWorker.cs b/dotnet/src/Microsoft.AutoGen/Core/IAgentWorker.cs index f26e30584ca6..a8be598aa7ca 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/IAgentWorker.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/IAgentWorker.cs @@ -5,10 +5,14 @@ namespace Microsoft.AutoGen.Core; public interface IAgentWorker { + IServiceProvider ServiceProvider { get; } ValueTask PublishEventAsync(CloudEvent evt, CancellationToken cancellationToken = default); ValueTask SendRequestAsync(Agent agent, RpcRequest request, CancellationToken cancellationToken = default); ValueTask SendResponseAsync(RpcResponse response, CancellationToken cancellationToken = default); ValueTask SendMessageAsync(Message message, CancellationToken cancellationToken = default); ValueTask StoreAsync(AgentState value, CancellationToken cancellationToken = default); ValueTask ReadAsync(AgentId agentId, CancellationToken cancellationToken = default); + ValueTask SubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default); + ValueTask UnsubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default); + ValueTask> GetSubscriptionsAsync(Type type); } diff --git a/dotnet/src/Microsoft.AutoGen/Core/IAgentWorkerExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core/IAgentWorkerExtensions.cs new file mode 100644 index 000000000000..53dd8890e974 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core/IAgentWorkerExtensions.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IAgentWorkerExtensions.cs + +using System.Diagnostics; +using Google.Protobuf.Collections; +using Microsoft.AutoGen.Contracts; +using Microsoft.Extensions.DependencyInjection; +using static Microsoft.AutoGen.Contracts.CloudEvent.Types; + +namespace Microsoft.AutoGen.Core; + +public static class IAgentWorkerExtensions +{ + public static (string?, string?) GetTraceIdAndState(IAgentWorker worker, IDictionary metadata) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + dcp.ExtractTraceIdAndState(metadata, + static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => + { + var metadata = (IDictionary)carrier!; + fieldValues = null; + metadata.TryGetValue(fieldName, out fieldValue); + }, + out var traceParent, + out var traceState); + return (traceParent, traceState); + } + public static (string?, string?) GetTraceIdAndState(IAgentWorker worker, MapField metadata) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + dcp.ExtractTraceIdAndState(metadata, + static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => + { + var metadata = (MapField)carrier!; + fieldValues = null; + metadata.TryGetValue(fieldName, out var ceValue); + fieldValue = ceValue?.CeString; + }, + out var traceParent, + out var traceState); + return (traceParent, traceState); + } + public static void Update(IAgentWorker worker, RpcRequest request, Activity? activity = null) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + dcp.Inject(activity, request.Metadata, static (carrier, key, value) => + { + var metadata = (IDictionary)carrier!; + if (metadata.TryGetValue(key, out _)) + { + metadata[key] = value; + } + else + { + metadata.Add(key, value); + } + }); + } + public static void Update(IAgentWorker worker, CloudEvent cloudEvent, Activity? activity = null) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + dcp.Inject(activity, cloudEvent.Attributes, static (carrier, key, value) => + { + var mapField = (MapField)carrier!; + if (mapField.TryGetValue(key, out var ceValue)) + { + mapField[key] = new CloudEventAttributeValue { CeString = value }; + } + else + { + mapField.Add(key, new CloudEventAttributeValue { CeString = value }); + } + }); + } + + public static IDictionary ExtractMetadata(IAgentWorker worker, IDictionary metadata) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + var baggage = dcp.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => + { + var metadata = (IDictionary)carrier!; + fieldValues = null; + metadata.TryGetValue(fieldName, out fieldValue); + }); + + return baggage as IDictionary ?? new Dictionary(); + } + public static IDictionary ExtractMetadata(IAgentWorker worker, MapField metadata) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + var baggage = dcp.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => + { + var metadata = (MapField)carrier!; + fieldValues = null; + metadata.TryGetValue(fieldName, out var ceValue); + fieldValue = ceValue?.CeString; + }); + + return baggage as IDictionary ?? new Dictionary(); + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Core/IHandle.cs b/dotnet/src/Microsoft.AutoGen/Core/IHandle.cs index 025ea8a5149c..cb1dd62de406 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/IHandle.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/IHandle.cs @@ -1,12 +1,20 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // IHandle.cs + +using Google.Protobuf; + namespace Microsoft.AutoGen.Core; -public interface IHandle -{ - Task HandleObject(object item); -} -public interface IHandle : IHandle +/// +/// Defines a handler interface for processing items of type . +/// +/// The type of item to be handled, which must implement . +public interface IHandle where T : IMessage { - Task Handle(T item); + /// + /// Handles the specified item asynchronously. + /// + /// The item to be handled. + /// A task that represents the asynchronous operation. + Task Handle(T item, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.AutoGen/Core/MessageExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core/MessageExtensions.cs new file mode 100644 index 000000000000..5ac9d43cf092 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core/MessageExtensions.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// MessageExtensions.cs + +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Core; + +/// +/// Provides extension methods for converting messages to and from various formats. +/// +public static class MessageExtensions +{ + private const string PROTO_DATA_CONTENT_TYPE = "application/x-protobuf"; + + /// + /// Converts a message to a CloudEvent. + /// + /// The type of the message. + /// The message to convert. + /// The key of the event, maps to the Topic Type + /// The topic of the event, + /// A CloudEvent representing the message. + public static CloudEvent ToCloudEvent(this T message, string key, string topic) where T : IMessage + { + return new CloudEvent + { + ProtoData = Any.Pack(message), + Type = message.Descriptor.FullName, + Source = topic, + Id = Guid.NewGuid().ToString(), + Attributes = { + { + "datacontenttype", new CloudEvent.Types.CloudEventAttributeValue { CeString = PROTO_DATA_CONTENT_TYPE } + }, + { + "subject", new CloudEvent.Types.CloudEventAttributeValue { CeString = key } + } + } + }; + } + + /// + /// Converts a CloudEvent back to a message. + /// + /// The type of the message. + /// The CloudEvent to convert. + /// The message represented by the CloudEvent. + public static T FromCloudEvent(this CloudEvent cloudEvent) where T : IMessage, new() + { + return cloudEvent.ProtoData.Unpack(); + } + + /// + public static string GetSubject(this CloudEvent cloudEvent) + { + return cloudEvent.Attributes["subject"].CeString; + } + + /// + /// Converts a state to an AgentState. + /// + /// The type of the state. + /// The state to convert. + /// The ID of the agent. + /// The ETag of the state. + /// An AgentState representing the state. + public static AgentState ToAgentState(this T state, AgentId agentId, string eTag) where T : IMessage + { + return new AgentState + { + ProtoData = Any.Pack(state), + AgentId = agentId, + ETag = eTag + }; + } + + /// + /// Converts an AgentState back to a state. + /// + /// The type of the state. + /// The AgentState to convert. + /// The state represented by the AgentState. + public static T FromAgentState(this AgentState state) where T : IMessage, new() + { + if (state.HasTextData == true) + { + if (typeof(T) == typeof(AgentState)) + { + return (T)(IMessage)state; + } + } + return state.ProtoData.Unpack(); + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Core/ReflectionHelper.cs b/dotnet/src/Microsoft.AutoGen/Core/ReflectionHelper.cs index 41b27ffee613..a977af69e79d 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/ReflectionHelper.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/ReflectionHelper.cs @@ -23,7 +23,7 @@ public static bool IsSubclassOfGeneric(Type type, Type genericBaseType) } return false; } - public static EventTypes GetAgentsMetadata(params Assembly[] assemblies) + public static AgentsMetadata GetAgentsMetadata(params Assembly[] assemblies) { var interfaceType = typeof(IMessage); var pairs = assemblies @@ -42,8 +42,12 @@ public static EventTypes GetAgentsMetadata(params Assembly[] assemblies) .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IHandle<>)) .Select(i => GetMessageDescriptor(i.GetGenericArguments().First())?.FullName ?? "").ToHashSet())) .ToDictionary(item => item.t, item => item.Item2); - - return new EventTypes(typeRegistry, types, eventsMap); + var topicsMap = assemblies + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => IsSubclassOfGeneric(type, typeof(Agent)) && !type.IsAbstract) + .Select(t => (t, t.GetCustomAttributes().Select(a => a.Topic).ToHashSet())) + .ToDictionary(item => item.t, item => item.Item2); + return new AgentsMetadata(typeRegistry, types, eventsMap, topicsMap); } /// diff --git a/dotnet/src/Microsoft.AutoGen/Contracts/TopicSubscriptionAttribute.cs b/dotnet/src/Microsoft.AutoGen/Core/TopicSubscriptionAttribute.cs similarity index 86% rename from dotnet/src/Microsoft.AutoGen/Contracts/TopicSubscriptionAttribute.cs rename to dotnet/src/Microsoft.AutoGen/Core/TopicSubscriptionAttribute.cs index ba17520f79b9..8c1e93e2f664 100644 --- a/dotnet/src/Microsoft.AutoGen/Contracts/TopicSubscriptionAttribute.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/TopicSubscriptionAttribute.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // TopicSubscriptionAttribute.cs -namespace Microsoft.AutoGen.Contracts; +namespace Microsoft.AutoGen.Core; [AttributeUsage(AttributeTargets.All)] public class TopicSubscriptionAttribute(string topic) : Attribute diff --git a/dotnet/src/Microsoft.AutoGen/Core/UninitializedAgentWorker.cs b/dotnet/src/Microsoft.AutoGen/Core/UninitializedAgentWorker.cs new file mode 100644 index 000000000000..fec7fef60cc5 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core/UninitializedAgentWorker.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// UninitializedAgentWorker.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Core; +public class UninitializedAgentWorker() : IAgentWorker +{ + public IServiceProvider ServiceProvider => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + internal const string AgentNotInitializedMessage = "Agent not initialized correctly. An Agent should never be directly intialized - it is always started by the AgentWorker from the Runtime (using the static Initialize() method)."; + public ValueTask PublishEventAsync(CloudEvent evt, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public ValueTask ReadAsync(AgentId agentId, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public ValueTask SendMessageAsync(Message message, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public ValueTask SendRequestAsync(Agent agent, RpcRequest request, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public ValueTask SendResponseAsync(RpcResponse response, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public ValueTask StoreAsync(AgentState value, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public ValueTask> GetSubscriptionsAsync(Type type) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public ValueTask SubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public ValueTask UnsubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage); + public class AgentInitalizedIncorrectlyException(string message) : Exception(message) + { + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IAgentGrain.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IAgentGrain.cs new file mode 100644 index 000000000000..947b6b0cbc0a --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IAgentGrain.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IAgentGrain.cs + +namespace Microsoft.AutoGen.Runtime.Grpc.Abstractions; + +internal interface IAgentGrain : IGrainWithStringKey +{ + ValueTask ReadStateAsync(); + ValueTask WriteStateAsync(Contracts.AgentState state, string eTag); +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IGateway.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IGateway.cs new file mode 100644 index 000000000000..02aeb6bf0c3f --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IGateway.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IGateway.cs +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Abstractions; + +public interface IGateway : IGrainObserver +{ + ValueTask InvokeRequestAsync(RpcRequest request); + ValueTask BroadcastEventAsync(CloudEvent evt); + ValueTask StoreAsync(Contracts.AgentState value); + ValueTask ReadAsync(AgentId agentId); + ValueTask RegisterAgentTypeAsync(RegisterAgentTypeRequest request); + ValueTask SubscribeAsync(SubscriptionRequest request); + ValueTask UnsubscribeAsync(SubscriptionRequest request); + ValueTask> GetSubscriptionsAsync(Type type); + Task SendMessageAsync(IConnection connection, CloudEvent cloudEvent); +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IRegistry.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IRegistry.cs new file mode 100644 index 000000000000..08007dd4ad62 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IRegistry.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IRegistry.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Abstractions; + +/// +/// Interface for managing agent registration, placement, and subscriptions. +/// +public interface IRegistry +{ + /// + /// Gets or places an agent based on the provided agent ID. + /// + /// The ID of the agent. + /// A tuple containing the worker and a boolean indicating if it's a new placement. + ValueTask<(IGateway? Worker, bool NewPlacement)> GetOrPlaceAgent(AgentId agentId); + + /// + /// Removes a worker from the registry. + /// + /// The worker to remove. + /// A task representing the asynchronous operation. + ValueTask RemoveWorker(IGateway worker); + + /// + /// Registers a new agent type with the specified worker. + /// + /// The request containing agent type details. + /// The worker to register the agent type with. + /// A task representing the asynchronous operation. + ValueTask RegisterAgentType(RegisterAgentTypeRequest request, IGateway worker); + + /// + /// Adds a new worker to the registry. + /// + /// The worker to add. + /// A task representing the asynchronous operation. + ValueTask AddWorker(IGateway worker); + + /// + /// Unregisters an agent type from the specified worker. + /// + /// The type of the agent to unregister. + /// The worker to unregister the agent type from. + /// A task representing the asynchronous operation. + ValueTask UnregisterAgentType(string type, IGateway worker); + + /// + /// Gets a compatible worker for the specified agent type. + /// + /// The type of the agent. + /// A task representing the asynchronous operation, with the compatible worker as the result. + ValueTask GetCompatibleWorker(string type); + + /// + /// Gets a list of agents subscribed to and handling the specified topic and event type. + /// + /// The topic to check subscriptions for. + /// The event type to check subscriptions for. + /// A task representing the asynchronous operation, with the list of agent IDs as the result. + ValueTask> GetSubscribedAndHandlingAgents(string topic, string eventType); + + /// + /// Subscribes an agent to a topic. + /// + /// The subscription request. + /// A task representing the asynchronous operation. + ValueTask SubscribeAsync(SubscriptionRequest request); + + /// + /// Unsubscribes an agent from a topic. + /// + /// The unsubscription request. + /// A task representing the asynchronous operation. + ValueTask UnsubscribeAsync(SubscriptionRequest request); // TODO: This should have its own request type. + + /// + /// Gets the subscriptions for a specified agent type. + /// + /// The type of the agent. + /// A task representing the asynchronous operation, with the subscriptions as the result. + ValueTask> GetSubscriptions(string agentType); +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IRegistryGrain.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IRegistryGrain.cs new file mode 100644 index 000000000000..6a5a8e725ecd --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Abstractions/IRegistryGrain.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IRegistryGrain.cs + +namespace Microsoft.AutoGen.Runtime.Grpc.Abstractions; + +/// +/// Orleans specific interface, needed to mark the key +/// +[Alias("Microsoft.AutoGen.Runtime.Grpc.Abstractions.IRegistryGrain")] +public interface IRegistryGrain : IRegistry, IGrainWithIntegerKey +{ } diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Microsoft.AutoGen.Runtime.Grpc.csproj b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Microsoft.AutoGen.Runtime.Grpc.csproj index 27474cef7900..b874a657d8f2 100644 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Microsoft.AutoGen.Runtime.Grpc.csproj +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Microsoft.AutoGen.Runtime.Grpc.csproj @@ -6,6 +6,7 @@ + diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/AgentWorkerHostingExtensions.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/AgentWorkerHostingExtensions.cs index bd2ecfa9a8a7..4c1ec149ab8a 100644 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/AgentWorkerHostingExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/AgentWorkerHostingExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Microsoft.AutoGen.Core; namespace Microsoft.AutoGen.Runtime.Grpc; public static class AgentWorkerHostingExtensions @@ -17,6 +18,10 @@ public static WebApplicationBuilder AddAgentService(this WebApplicationBuilder b builder.Services.TryAddSingleton(DistributedContextPropagator.Current); builder.Services.AddGrpc(); + builder.Services.AddKeyedSingleton("AgentsMetadata", (sp, key) => + { + return ReflectionHelper.GetAgentsMetadata(AppDomain.CurrentDomain.GetAssemblies()); + }); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => (IHostedService)sp.GetRequiredService()); diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcGateway.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcGateway.cs index 2d63ab0870ca..57df08bf327b 100644 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcGateway.cs +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcGateway.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using Grpc.Core; using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Runtime.Grpc.Abstractions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -14,22 +15,21 @@ public sealed class GrpcGateway : BackgroundService, IGateway private static readonly TimeSpan s_agentResponseTimeout = TimeSpan.FromSeconds(30); private readonly ILogger _logger; private readonly IClusterClient _clusterClient; - private readonly ConcurrentDictionary _agentState = new(); + //private readonly ConcurrentDictionary _agentState = new(); private readonly IRegistryGrain _gatewayRegistry; - private readonly ISubscriptionsGrain _subscriptions; private readonly IGateway _reference; // The agents supported by each worker process. private readonly ConcurrentDictionary> _supportedAgentTypes = []; public readonly ConcurrentDictionary _workers = new(); + internal readonly ConcurrentDictionary _workersByConnection = new(); private readonly ConcurrentDictionary _subscriptionsByAgentType = new(); private readonly ConcurrentDictionary> _subscriptionsByTopic = new(); + private readonly ISubscriptionsGrain _subscriptions; // The mapping from agent id to worker process. private readonly ConcurrentDictionary<(string Type, string Key), GrpcWorkerConnection> _agentDirectory = new(); // RPC private readonly ConcurrentDictionary<(GrpcWorkerConnection, string), TaskCompletionSource> _pendingRequests = new(); - // InMemory Message Queue - public GrpcGateway(IClusterClient clusterClient, ILogger logger) { _logger = logger; @@ -38,31 +38,89 @@ public GrpcGateway(IClusterClient clusterClient, ILogger logger) _gatewayRegistry = clusterClient.GetGrain(0); _subscriptions = clusterClient.GetGrain(0); } - public async ValueTask BroadcastEvent(CloudEvent evt) + public async ValueTask InvokeRequestAsync(RpcRequest request, CancellationToken cancellationToken = default) { - var tasks = new List(_workers.Count); - foreach (var (_, connection) in _supportedAgentTypes) + var agentId = (request.Target.Type, request.Target.Key); + if (!_agentDirectory.TryGetValue(agentId, out var connection) || connection.Completion.IsCompleted == true) { - - tasks.Add(this.SendMessageAsync((IConnection)connection[0], evt, default)); + // Activate the agent on a compatible worker process. + if (_supportedAgentTypes.TryGetValue(request.Target.Type, out var workers)) + { + connection = workers[Random.Shared.Next(workers.Count)]; + _agentDirectory[agentId] = connection; + } + else + { + return new(new RpcResponse { Error = "Agent not found." }); + } } - await Task.WhenAll(tasks).ConfigureAwait(false); + // Proxy the request to the agent. + var originalRequestId = request.RequestId; + var newRequestId = Guid.NewGuid().ToString(); + var completion = _pendingRequests[(connection, newRequestId)] = new(TaskCreationOptions.RunContinuationsAsynchronously); + request.RequestId = newRequestId; + await connection.ResponseStream.WriteAsync(new Message { Request = request }, cancellationToken).ConfigureAwait(false); + // Wait for the response and send it back to the caller. + var response = await completion.Task.WaitAsync(s_agentResponseTimeout); + response.RequestId = originalRequestId; + return response; } - //intetionally not static so can be called by some methods implemented in base class - public async Task SendMessageAsync(IConnection connection, CloudEvent cloudEvent, CancellationToken cancellationToken = default) + public async ValueTask StoreAsync(AgentState value, CancellationToken cancellationToken = default) { - var queue = (GrpcWorkerConnection)connection; - await queue.ResponseStream.WriteAsync(new Message { CloudEvent = cloudEvent }, cancellationToken).ConfigureAwait(false); + _ = value.AgentId ?? throw new ArgumentNullException(nameof(value.AgentId)); + var agentState = _clusterClient.GetGrain($"{value.AgentId.Type}:{value.AgentId.Key}"); + await agentState.WriteStateAsync(value, value.ETag); } - private void DispatchResponse(GrpcWorkerConnection connection, RpcResponse response) + public async ValueTask ReadAsync(AgentId agentId, CancellationToken cancellationToken = default) { - if (!_pendingRequests.TryRemove((connection, response.RequestId), out var completion)) + var agentState = _clusterClient.GetGrain($"{agentId.Type}:{agentId.Key}"); + return await agentState.ReadStateAsync(); + } + public async ValueTask RegisterAgentTypeAsync(RegisterAgentTypeRequest request, CancellationToken cancellationToken = default) + { + try { - _logger.LogWarning("Received response for unknown request."); - return; + var connection = _workersByConnection[request.RequestId]; + connection.AddSupportedType(request.Type); + _supportedAgentTypes.GetOrAdd(request.Type, _ => []).Add(connection); + + await _gatewayRegistry.RegisterAgentType(request, _reference).ConfigureAwait(true); + return new RegisterAgentTypeResponse + { + Success = true, + RequestId = request.RequestId + }; + } + catch (Exception ex) + { + return new RegisterAgentTypeResponse + { + Success = false, + RequestId = request.RequestId, + Error = ex.Message + }; + } + } + public async ValueTask SubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default) + { + try + { + await _gatewayRegistry.SubscribeAsync(request).ConfigureAwait(true); + return new SubscriptionResponse + { + Success = true, + RequestId = request.RequestId + }; + } + catch (Exception ex) + { + return new SubscriptionResponse + { + Success = false, + RequestId = request.RequestId, + Error = ex.Message + }; } - // Complete the request. - completion.SetResult(response); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { @@ -87,7 +145,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogWarning(exception, "Error removing worker from registry."); } } - //new is intentional... + internal Task ConnectToWorkerProcess(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + _logger.LogInformation("Received new connection from {Peer}.", context.Peer); + var workerProcess = new GrpcWorkerConnection(this, requestStream, responseStream, context); + _workers[workerProcess] = workerProcess; + _workersByConnection[context.Peer] = workerProcess; + return workerProcess.Completion; + } + internal async Task SendMessageAsync(GrpcWorkerConnection connection, CloudEvent cloudEvent, CancellationToken cancellationToken = default) + { + await connection.ResponseStream.WriteAsync(new Message { CloudEvent = cloudEvent }, cancellationToken).ConfigureAwait(false); + } internal async Task OnReceivedMessageAsync(GrpcWorkerConnection connection, Message message) { _logger.LogInformation("Received message {Message} from connection {Connection}.", message, connection); @@ -105,8 +174,8 @@ internal async Task OnReceivedMessageAsync(GrpcWorkerConnection connection, Mess case Message.MessageOneofCase.RegisterAgentTypeRequest: await RegisterAgentTypeAsync(connection, message.RegisterAgentTypeRequest); break; - case Message.MessageOneofCase.AddSubscriptionRequest: - await AddSubscriptionAsync(connection, message.AddSubscriptionRequest); + case Message.MessageOneofCase.SubscriptionRequest: + await AddSubscriptionAsync(connection, message.SubscriptionRequest); break; default: // if it wasn't recognized return bad request @@ -114,48 +183,22 @@ internal async Task OnReceivedMessageAsync(GrpcWorkerConnection connection, Mess break; }; } - private async ValueTask RespondBadRequestAsync(GrpcWorkerConnection connection, string error) - { - throw new RpcException(new Status(StatusCode.InvalidArgument, error)); - } - - // agentype:rpc_request={requesting_agent_id} - // {genttype}:rpc_response={request_id} - private async ValueTask AddSubscriptionAsync(GrpcWorkerConnection connection, AddSubscriptionRequest request) + private void DispatchResponse(GrpcWorkerConnection connection, RpcResponse response) { - var topic = ""; - var agentType = ""; - if (request.Subscription.TypePrefixSubscription is not null) - { - topic = request.Subscription.TypePrefixSubscription.TopicTypePrefix; - agentType = request.Subscription.TypePrefixSubscription.AgentType; - } - else if (request.Subscription.TypeSubscription is not null) + if (!_pendingRequests.TryRemove((connection, response.RequestId), out var completion)) { - topic = request.Subscription.TypeSubscription.TopicType; - agentType = request.Subscription.TypeSubscription.AgentType; + _logger.LogWarning("Received response for unknown request id: {RequestId}.", response.RequestId); + return; } - _subscriptionsByAgentType[agentType] = request.Subscription; - _subscriptionsByTopic.GetOrAdd(topic, _ => []).Add(agentType); - await _subscriptions.SubscribeAsync(topic, agentType); - //var response = new AddSubscriptionResponse { RequestId = request.RequestId, Error = "", Success = true }; - Message response = new() - { - AddSubscriptionResponse = new() - { - RequestId = request.RequestId, - Error = "", - Success = true - } - }; - await connection.ResponseStream.WriteAsync(response).ConfigureAwait(false); + // Complete the request. + completion.SetResult(response); } private async ValueTask RegisterAgentTypeAsync(GrpcWorkerConnection connection, RegisterAgentTypeRequest msg) { connection.AddSupportedType(msg.Type); _supportedAgentTypes.GetOrAdd(msg.Type, _ => []).Add(connection); - await _gatewayRegistry.RegisterAgentType(msg.Type, _reference).ConfigureAwait(true); + await _gatewayRegistry.RegisterAgentType(msg, _reference).ConfigureAwait(true); Message response = new() { RegisterAgentTypeResponse = new() @@ -169,12 +212,25 @@ private async ValueTask RegisterAgentTypeAsync(GrpcWorkerConnection connection, } private async ValueTask DispatchEventAsync(CloudEvent evt) { + + var registry = _clusterClient.GetGrain(0); + //intentionally blocking + var targetAgentTypes = await registry.GetSubscribedAndHandlingAgents(evt.Source, evt.Type).ConfigureAwait(true); + if (targetAgentTypes.Count == 0) + { + _logger.LogWarning("No agents found registered for event {Event}.", evt); + } + else + { + await DispatchEventToAgentsAsync(targetAgentTypes, evt).ConfigureAwait(false); + } + // alternate path // get the event type and then send to all agents that are subscribed to that event type var eventType = evt.Type; // ensure that we get agentTypes as an async enumerable list - try to get the value of agentTypes by topic and then cast it to an async enumerable list if (_subscriptionsByTopic.TryGetValue(eventType, out var agentTypes)) { - await DispatchEventToAgentsAsync(agentTypes, evt); + await DispatchEventToAgentsAsync(agentTypes, evt: evt).ConfigureAwait(false); } // instead of an exact match, we can also check for a prefix match where key starts with the eventType else if (_subscriptionsByTopic.Keys.Any(key => key.StartsWith(eventType))) @@ -195,21 +251,6 @@ private async ValueTask DispatchEventAsync(CloudEvent evt) _logger.LogWarning("No agent types found for event type {EventType}.", eventType); } } - private async ValueTask DispatchEventToAgentsAsync(IEnumerable agentTypes, CloudEvent evt) - { - var tasks = new List(agentTypes.Count()); - foreach (var agentType in agentTypes) - { - if (_supportedAgentTypes.TryGetValue(agentType, out var connections)) - { - foreach (var connection in connections) - { - tasks.Add(this.SendMessageAsync(connection, evt)); - } - } - } - await Task.WhenAll(tasks).ConfigureAwait(false); - } private async ValueTask DispatchRequestAsync(GrpcWorkerConnection connection, RpcRequest request) { var requestId = request.RequestId; @@ -229,11 +270,9 @@ await InvokeRequestDelegate(connection, request, async request => // TODO// Activate the worker: load state } // Forward the message to the gateway and return the result. - return await gateway.InvokeRequest(request).ConfigureAwait(true); - }); - //} + return await gateway.InvokeRequestAsync(request).ConfigureAwait(true); + }).ConfigureAwait(false); } - private static async Task InvokeRequestDelegate(GrpcWorkerConnection connection, RpcRequest request, Func> func) { try @@ -247,27 +286,6 @@ private static async Task InvokeRequestDelegate(GrpcWorkerConnection connection, await connection.ResponseStream.WriteAsync(new Message { Response = new RpcResponse { RequestId = request.RequestId, Error = ex.Message } }).ConfigureAwait(false); } } - internal Task ConnectToWorkerProcess(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) - { - _logger.LogInformation("Received new connection from {Peer}.", context.Peer); - var workerProcess = new GrpcWorkerConnection(this, requestStream, responseStream, context); - _workers[workerProcess] = workerProcess; - return workerProcess.Completion; - } - public async ValueTask StoreAsync(AgentState value) - { - var agentId = value.AgentId ?? throw new ArgumentNullException(nameof(value.AgentId)); - _agentState[agentId.Key] = value; - } - - public async ValueTask ReadAsync(AgentId agentId) - { - if (_agentState.TryGetValue(agentId.Key, out var state)) - { - return state; - } - return new AgentState { AgentId = agentId }; - } internal void OnRemoveWorkerProcess(GrpcWorkerConnection workerProcess) { _workers.TryRemove(workerProcess, out _); @@ -288,41 +306,142 @@ internal void OnRemoveWorkerProcess(GrpcWorkerConnection workerProcess) } } } - public async ValueTask InvokeRequest(RpcRequest request, CancellationToken cancellationToken = default) + private static async ValueTask RespondBadRequestAsync(GrpcWorkerConnection connection, string error) + { + throw new RpcException(new Status(StatusCode.InvalidArgument, error)); + } + private async ValueTask AddSubscriptionAsync(GrpcWorkerConnection connection, SubscriptionRequest request) { - (string Type, string Key) agentId = (request.Target.Type, request.Target.Key); - if (!_agentDirectory.TryGetValue(agentId, out var connection) || connection.Completion.IsCompleted) + var topic = ""; + var agentType = ""; + if (request.Subscription.TypePrefixSubscription is not null) { - // Activate the agent on a compatible worker process. - if (_supportedAgentTypes.TryGetValue(request.Target.Type, out var workers)) + topic = request.Subscription.TypePrefixSubscription.TopicTypePrefix; + agentType = request.Subscription.TypePrefixSubscription.AgentType; + } + else if (request.Subscription.TypeSubscription is not null) + { + topic = request.Subscription.TypeSubscription.TopicType; + agentType = request.Subscription.TypeSubscription.AgentType; + } + _subscriptionsByAgentType[agentType] = request.Subscription; + _subscriptionsByTopic.GetOrAdd(topic, _ => []).Add(agentType); + await _subscriptions.SubscribeAsync(topic, agentType); + //var response = new SubscriptionResponse { RequestId = request.RequestId, Error = "", Success = true }; + Message response = new() + { + SubscriptionResponse = new() { - connection = workers[Random.Shared.Next(workers.Count)]; - _agentDirectory[agentId] = connection; + RequestId = request.RequestId, + Error = "", + Success = true } - else + }; + await connection.ResponseStream.WriteAsync(response).ConfigureAwait(false); + } + private async ValueTask DispatchEventToAgentsAsync(IEnumerable agentTypes, CloudEvent evt) + { + var tasks = new List(agentTypes.Count()); + foreach (var agentType in agentTypes) + { + if (_supportedAgentTypes.TryGetValue(agentType, out var connections)) { - return new(new RpcResponse { Error = "Agent not found." }); + foreach (var connection in connections) + { + tasks.Add(this.SendMessageAsync(connection, evt)); + } } } - // Proxy the request to the agent. - var originalRequestId = request.RequestId; - var newRequestId = Guid.NewGuid().ToString(); - var completion = _pendingRequests[(connection, newRequestId)] = new(TaskCreationOptions.RunContinuationsAsynchronously); - request.RequestId = newRequestId; - await connection.ResponseStream.WriteAsync(new Message { Request = request }, cancellationToken).ConfigureAwait(false); - // Wait for the response and send it back to the caller. - var response = await completion.Task.WaitAsync(s_agentResponseTimeout); - response.RequestId = originalRequestId; - return response; + await Task.WhenAll(tasks).ConfigureAwait(false); } + public async ValueTask BroadcastEventAsync(CloudEvent evt, CancellationToken cancellationToken = default) + { + var tasks = new List(_workers.Count); + foreach (var (_, connection) in _supportedAgentTypes) + { - async ValueTask IGateway.InvokeRequest(RpcRequest request) + tasks.Add(this.SendMessageAsync((IConnection)connection[0], evt, default)); + } + await Task.WhenAll(tasks).ConfigureAwait(false); + } + Task IGateway.SendMessageAsync(IConnection connection, CloudEvent cloudEvent) { - return await this.InvokeRequest(request).ConfigureAwait(false); + return this.SendMessageAsync(connection, cloudEvent, default); + } + public async Task SendMessageAsync(IConnection connection, CloudEvent cloudEvent, CancellationToken cancellationToken = default) + { + var queue = (GrpcWorkerConnection)connection; + await queue.ResponseStream.WriteAsync(new Message { CloudEvent = cloudEvent }, cancellationToken).ConfigureAwait(false); } - Task IGateway.SendMessageAsync(IConnection connection, CloudEvent cloudEvent) + public async ValueTask UnsubscribeAsync(SubscriptionRequest request, CancellationToken cancellationToken = default) + { + try + { + await _gatewayRegistry.UnsubscribeAsync(request).ConfigureAwait(true); + return new SubscriptionResponse + { + Success = true, + RequestId = request.RequestId + }; + } + catch (Exception ex) + { + return new SubscriptionResponse + { + Success = false, + RequestId = request.RequestId, + Error = ex.Message + }; + } + } + + public ValueTask> GetSubscriptionsAsync(Type type, CancellationToken cancellationToken = default) + { + return _gatewayRegistry.GetSubscriptions(nameof(type)); + } + public ValueTask> GetSubscriptionsAsync(string type, CancellationToken cancellationToken = default) + { + return _gatewayRegistry.GetSubscriptions(type); + } + + async ValueTask IGateway.InvokeRequestAsync(RpcRequest request) + { + return await InvokeRequestAsync(request, default).ConfigureAwait(false); + } + + async ValueTask IGateway.BroadcastEventAsync(CloudEvent evt) + { + await BroadcastEventAsync(evt, default).ConfigureAwait(false); + } + + ValueTask IGateway.StoreAsync(AgentState value) + { + return StoreAsync(value, default); + } + + ValueTask IGateway.ReadAsync(AgentId agentId) + { + return ReadAsync(agentId, default); + } + + ValueTask IGateway.RegisterAgentTypeAsync(RegisterAgentTypeRequest request) + { + return RegisterAgentTypeAsync(request, default); + } + + ValueTask IGateway.SubscribeAsync(SubscriptionRequest request) + { + return SubscribeAsync(request, default); + } + + ValueTask IGateway.UnsubscribeAsync(SubscriptionRequest request) + { + return UnsubscribeAsync(request, default); + } + + ValueTask> IGateway.GetSubscriptionsAsync(Type type) { - return this.SendMessageAsync(connection, cloudEvent); + return GetSubscriptionsAsync(type, default); } } diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcGatewayService.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcGatewayService.cs index ca4ffbb30c3e..18ca0b407a9a 100644 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcGatewayService.cs +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcGatewayService.cs @@ -7,7 +7,7 @@ namespace Microsoft.AutoGen.Runtime.Grpc; // gRPC service which handles communication between the agent worker and the cluster. -internal sealed class GrpcGatewayService : AgentRpc.AgentRpcBase +public sealed class GrpcGatewayService : AgentRpc.AgentRpcBase { private readonly GrpcGateway Gateway; public GrpcGatewayService(GrpcGateway gateway) @@ -34,7 +34,6 @@ public override async Task GetState(AgentId request, ServerCal var state = await Gateway.ReadAsync(request); return new GetStateResponse { AgentState = state }; } - public override async Task SaveState(AgentState request, ServerCallContext context) { await Gateway.StoreAsync(request); @@ -43,4 +42,24 @@ public override async Task SaveState(AgentState request, Serv Success = true // TODO: Implement error handling }; } + public override async Task Subscribe(SubscriptionRequest request, ServerCallContext context) + { + request.RequestId = context.Peer; + return await Gateway.SubscribeAsync(request).ConfigureAwait(true); + } + public override async Task Unsubscribe(SubscriptionRequest request, ServerCallContext context) + { + request.RequestId = context.Peer; + return await Gateway.UnsubscribeAsync(request).ConfigureAwait(true); + } + public override async Task GetSubscriptions(AgentId request, ServerCallContext context) + { + var subscriptions = await Gateway.GetSubscriptionsAsync(request.Type); + return new SubscriptionList { Subscriptions = { subscriptions } }; + } + public override async Task RegisterAgent(RegisterAgentTypeRequest request, ServerCallContext context) + { + request.RequestId = context.Peer; + return await Gateway.RegisterAgentTypeAsync(request).ConfigureAwait(true); + } } diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcWorkerConnection.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcWorkerConnection.cs index 315cd81feb1c..00c777953688 100644 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcWorkerConnection.cs +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Grpc/GrpcWorkerConnection.cs @@ -10,14 +10,14 @@ namespace Microsoft.AutoGen.Runtime.Grpc; internal sealed class GrpcWorkerConnection : IAsyncDisposable, IConnection { private static long s_nextConnectionId; - private readonly Task _readTask; - private readonly Task _writeTask; + private Task _readTask = Task.CompletedTask; + private Task _writeTask = Task.CompletedTask; private readonly string _connectionId = Interlocked.Increment(ref s_nextConnectionId).ToString(); private readonly object _lock = new(); private readonly HashSet _supportedTypes = []; private readonly GrpcGateway _gateway; private readonly CancellationTokenSource _shutdownCancellationToken = new(); - + public Task Completion { get; private set; } = Task.CompletedTask; public GrpcWorkerConnection(GrpcGateway agentWorker, IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) { _gateway = agentWorker; @@ -25,7 +25,9 @@ public GrpcWorkerConnection(GrpcGateway agentWorker, IAsyncStreamReader ResponseStream = responseStream; ServerCallContext = context; _outboundMessages = Channel.CreateUnbounded(new UnboundedChannelOptions { AllowSynchronousContinuations = true, SingleReader = true, SingleWriter = false }); - + } + public Task Connect() + { var didSuppress = false; if (!ExecutionContext.IsFlowSuppressed()) { @@ -46,7 +48,7 @@ public GrpcWorkerConnection(GrpcGateway agentWorker, IAsyncStreamReader } } - Completion = Task.WhenAll(_readTask, _writeTask); + return Completion = Task.WhenAll(_readTask, _writeTask); } public IAsyncStreamReader RequestStream { get; } @@ -75,9 +77,6 @@ public async Task SendMessage(Message message) { await _outboundMessages.Writer.WriteAsync(message).ConfigureAwait(false); } - - public Task Completion { get; } - public async Task RunReadPump() { await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/IGateway.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/IGateway.cs deleted file mode 100644 index 463ae4e532af..000000000000 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/IGateway.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// IGateway.cs -using Microsoft.AutoGen.Contracts; - -namespace Microsoft.AutoGen.Runtime.Grpc; - -public interface IGateway : IGrainObserver -{ - ValueTask InvokeRequest(RpcRequest request); - ValueTask BroadcastEvent(CloudEvent evt); - ValueTask StoreAsync(AgentState value); - ValueTask ReadAsync(AgentId agentId); - Task SendMessageAsync(IConnection connection, CloudEvent cloudEvent); -} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/AgentStateGrain.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/AgentStateGrain.cs index 9d46be929ea9..e926f374a61e 100644 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/AgentStateGrain.cs +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/AgentStateGrain.cs @@ -2,10 +2,11 @@ // AgentStateGrain.cs using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Runtime.Grpc.Abstractions; namespace Microsoft.AutoGen.Runtime.Grpc; -internal sealed class AgentStateGrain([PersistentState("state", "AgentStateStore")] IPersistentState state) : Grain, IAgentState +internal sealed class AgentStateGrain([PersistentState("state", "AgentStateStore")] IPersistentState state) : Grain, IAgentState, IAgentGrain { /// public async ValueTask WriteStateAsync(AgentState newState, string eTag, CancellationToken cancellationToken = default) @@ -33,4 +34,14 @@ public ValueTask ReadStateAsync(CancellationToken cancellationToken { return ValueTask.FromResult(state.State); } + + ValueTask IAgentGrain.ReadStateAsync() + { + return ReadStateAsync(); + } + + ValueTask IAgentGrain.WriteStateAsync(AgentState state, string eTag) + { + return WriteStateAsync(state, eTag); + } } diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/AgentsRegistryState.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/AgentsRegistryState.cs new file mode 100644 index 000000000000..3e69bd3cc3a9 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/AgentsRegistryState.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentsRegistryState.cs + +namespace Microsoft.AutoGen.Runtime.Grpc; + +public class AgentsRegistryState +{ + public Dictionary> AgentsToEventsMap { get; set; } = []; + public Dictionary> AgentsToTopicsMap { get; set; } = []; + public Dictionary> TopicToAgentTypesMap { get; set; } = []; + public Dictionary> EventsToAgentTypesMap { get; set; } = []; +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/IRegistryGrain.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/IRegistryGrain.cs deleted file mode 100644 index 1c817add3074..000000000000 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/IRegistryGrain.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// IRegistryGrain.cs -using Microsoft.AutoGen.Contracts; - -namespace Microsoft.AutoGen.Runtime.Grpc; - -public interface IRegistryGrain : IGrainWithIntegerKey -{ - ValueTask<(IGateway? Worker, bool NewPlacement)> GetOrPlaceAgent(AgentId agentId); - ValueTask RemoveWorker(IGateway worker); - ValueTask RegisterAgentType(string type, IGateway worker); - ValueTask AddWorker(IGateway worker); - ValueTask UnregisterAgentType(string type, IGateway worker); - ValueTask GetCompatibleWorker(string type); -} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/OrleansRuntimeHostingExtenions.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/OrleansRuntimeHostingExtenions.cs index 4b8a290df031..e83db26ad0b7 100644 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/OrleansRuntimeHostingExtenions.cs +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/OrleansRuntimeHostingExtenions.cs @@ -16,7 +16,6 @@ public static class OrleansRuntimeHostingExtenions public static WebApplicationBuilder AddOrleans(this WebApplicationBuilder builder) { builder.Services.AddSerializer(serializer => serializer.AddProtobufSerializer()); - builder.Services.AddSingleton(); // Ensure Orleans is added before the hosted service to guarantee that it starts first. //TODO: make all of this configurable @@ -28,6 +27,7 @@ public static WebApplicationBuilder AddOrleans(this WebApplicationBuilder builde siloBuilder.UseLocalhostClustering() .AddMemoryStreams("StreamProvider") .AddMemoryGrainStorage("PubSubStore") + .AddMemoryGrainStorage("AgentRegistryStore") .AddMemoryGrainStorage("AgentStateStore"); siloBuilder.UseInMemoryReminderService(); @@ -40,12 +40,7 @@ public static WebApplicationBuilder AddOrleans(this WebApplicationBuilder builde var cosmosDbconnectionString = builder.Configuration.GetValue("Orleans:CosmosDBConnectionString") ?? throw new ConfigurationErrorsException( "Orleans:CosmosDBConnectionString is missing from configuration. This is required for persistence in production environments."); - siloBuilder.Configure(options => - { - //TODO: make this configurable - options.ClusterId = "AutoGen-cluster"; - options.ServiceId = "AutoGen-cluster"; - }); + siloBuilder.Configure(options => { options.ResponseTimeout = TimeSpan.FromMinutes(3); diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/RegistryGrain.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/RegistryGrain.cs index 8de1618c6002..c3fbd58dbd43 100644 --- a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/RegistryGrain.cs +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/RegistryGrain.cs @@ -2,10 +2,10 @@ // RegistryGrain.cs using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Runtime.Grpc.Abstractions; namespace Microsoft.AutoGen.Runtime.Grpc; - -internal sealed class RegistryGrain : Grain, IRegistryGrain +internal sealed class RegistryGrain([PersistentState("state", "AgentRegistryStore")] IPersistentState state) : Grain, IRegistryGrain { // TODO: use persistent state for some of these or (better) extend Orleans to implement some of this natively. private readonly Dictionary _workerStates = new(); @@ -18,9 +18,19 @@ public override Task OnActivateAsync(CancellationToken cancellationToken) this.RegisterGrainTimer(static state => state.PurgeInactiveWorkers(), this, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30)); return base.OnActivateAsync(cancellationToken); } + + public ValueTask> GetSubscribedAndHandlingAgents(string topic, string eventType) + { + // get all agent types that are subscribed to the topic + var subscribedAgents = state.State.TopicToAgentTypesMap[topic]; + // get all agent types that are handling the event + var handlingAgents = state.State.EventsToAgentTypesMap[eventType]; + // return the intersection of the two sets + return new(subscribedAgents.Intersect(handlingAgents).ToList()); + } public ValueTask<(IGateway? Worker, bool NewPlacement)> GetOrPlaceAgent(AgentId agentId) { - // TODO: + // TODO: Clarify the logic bool isNewPlacement; if (!_agentDirectory.TryGetValue((agentId.Type, agentId.Key), out var worker) || !_workerStates.ContainsKey(worker)) { @@ -58,20 +68,47 @@ public ValueTask RemoveWorker(IGateway worker) } return ValueTask.CompletedTask; } - public ValueTask RegisterAgentType(string type, IGateway worker) + public async ValueTask RegisterAgentType(RegisterAgentTypeRequest registration, IGateway worker) { - if (!_supportedAgentTypes.TryGetValue(type, out var supportedAgentTypes)) + if (!_supportedAgentTypes.TryGetValue(registration.Type, out var supportedAgentTypes)) { - supportedAgentTypes = _supportedAgentTypes[type] = []; + supportedAgentTypes = _supportedAgentTypes[registration.Type] = []; } if (!supportedAgentTypes.Contains(worker)) { supportedAgentTypes.Add(worker); } + var workerState = GetOrAddWorker(worker); - workerState.SupportedTypes.Add(type); - return ValueTask.CompletedTask; + workerState.SupportedTypes.Add(registration.Type); + state.State.AgentsToEventsMap[registration.Type] = new HashSet(registration.Events); + state.State.AgentsToTopicsMap[registration.Type] = new HashSet(registration.Topics); + + // construct the inverse map for topics and agent types + foreach (var topic in registration.Topics) + { + if (!state.State.TopicToAgentTypesMap.TryGetValue(topic, out var topicSet)) + { + topicSet = new HashSet(); + state.State.TopicToAgentTypesMap[topic] = topicSet; + } + + topicSet.Add(registration.Type); + } + + // construct the inverse map for events and agent types + foreach (var evt in registration.Events) + { + if (!state.State.EventsToAgentTypesMap.TryGetValue(evt, out var eventSet)) + { + eventSet = new HashSet(); + state.State.EventsToAgentTypesMap[evt] = eventSet; + } + + eventSet.Add(registration.Type); + } + await state.WriteStateAsync().ConfigureAwait(false); } public ValueTask AddWorker(IGateway worker) { @@ -135,9 +172,98 @@ private WorkerState GetOrAddWorker(IGateway worker) return null; } + public async ValueTask SubscribeAsync(SubscriptionRequest sub) + { + switch (sub.Subscription.SubscriptionCase) + { + //TODO: this doesnt look right + case Subscription.SubscriptionOneofCase.TypePrefixSubscription: + break; + case Subscription.SubscriptionOneofCase.TypeSubscription: + { + // add the topic to the set of topics for the agent type + state.State.AgentsToTopicsMap.TryGetValue(sub.Subscription.TypeSubscription.AgentType, out var topics); + if (topics is null) + { + topics = new HashSet(); + state.State.AgentsToTopicsMap[sub.Subscription.TypeSubscription.AgentType] = topics; + } + topics.Add(sub.Subscription.TypeSubscription.TopicType); + + // add the agent type to the set of agent types for the topic + state.State.TopicToAgentTypesMap.TryGetValue(sub.Subscription.TypeSubscription.TopicType, out var agents); + if (agents is null) + { + agents = new HashSet(); + state.State.TopicToAgentTypesMap[sub.Subscription.TypeSubscription.TopicType] = agents; + } + agents.Add(sub.Subscription.TypeSubscription.AgentType); + break; + } + default: + throw new InvalidOperationException("Invalid subscription type"); + } + await state.WriteStateAsync().ConfigureAwait(false); + } + public async ValueTask UnsubscribeAsync(SubscriptionRequest request) + { + switch (request.Subscription.SubscriptionCase) + { + case Subscription.SubscriptionOneofCase.TypePrefixSubscription: + break; + case Subscription.SubscriptionOneofCase.TypeSubscription: + { + // remove the topic from the set of topics for the agent type + state.State.AgentsToTopicsMap.TryGetValue(request.Subscription.TypeSubscription.AgentType, out var topics); + if (topics is not null) + { + while (topics.Remove(request.Subscription.TypeSubscription.TopicType)) + { + // ensures all instances are removed + } + } + + // remove the agent type from the set of agent types for the topic + state.State.TopicToAgentTypesMap.TryGetValue(request.Subscription.TypeSubscription.TopicType, out var agents); + if (agents is not null) + { + while (agents.Remove(request.Subscription.TypeSubscription.AgentType)) + { + // ensures all instances are removed + } + } + break; + } + default: + throw new InvalidOperationException("Invalid subscription type"); + } + await state.WriteStateAsync().ConfigureAwait(false); + } + + public ValueTask> GetSubscriptions(string agentType) + { + var subscriptions = new List(); + if (state.State.AgentsToTopicsMap.TryGetValue(agentType, out var topics)) + { + foreach (var topic in topics) + { + subscriptions.Add(new Subscription + { + TypeSubscription = new TypeSubscription + { + AgentType = agentType, + TopicType = topic + } + }); + } + } + return new(subscriptions); + } + private sealed class WorkerState { public HashSet SupportedTypes { get; set; } = []; public DateTimeOffset LastSeen { get; set; } } } + diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/AgentIdSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/AgentIdSurrogate.cs new file mode 100644 index 000000000000..ddef9e997575 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/AgentIdSurrogate.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentIdSurrogate.cs + +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentIdSurrogate.cs +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct AgentIdSurrogate +{ + [Id(0)] + public string Key; + [Id(1)] + public string Type; +} + +[RegisterConverter] +public sealed class AgentIdSurrogateConverter : + IConverter +{ + public AgentId ConvertFromSurrogate( + in AgentIdSurrogate surrogate) => + new AgentId + { + Key = surrogate.Key, + Type = surrogate.Type + }; + + public AgentIdSurrogate ConvertToSurrogate( + in AgentId value) => + new AgentIdSurrogate + { + Key = value.Key, + Type = value.Type + }; +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/AgentStateSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/AgentStateSurrogate.cs new file mode 100644 index 000000000000..a5291f942155 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/AgentStateSurrogate.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentStateSurrogate.cs + +using Google.Protobuf; +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct AgentStateSurrogate +{ + [Id(0)] + public string Id; + [Id(1)] + public string TextData; + [Id(2)] + public ByteString BinaryData; + [Id(3)] + public AgentId AgentId; + [Id(4)] + public string Etag; + [Id(5)] + public ByteString ProtoData; +} + +[RegisterConverter] +public sealed class AgentStateSurrogateConverter : + IConverter +{ + public AgentState ConvertFromSurrogate( + in AgentStateSurrogate surrogate) + { + var agentState = new AgentState + { + AgentId = surrogate.AgentId, + BinaryData = surrogate.BinaryData, + TextData = surrogate.TextData, + ETag = surrogate.Etag + }; + //agentState.ProtoData = surrogate.ProtoData; + return agentState; + } + + public AgentStateSurrogate ConvertToSurrogate( + in AgentState value) => + new AgentStateSurrogate + { + AgentId = value.AgentId, + BinaryData = value.BinaryData, + TextData = value.TextData, + Etag = value.ETag, + //ProtoData = value.ProtoData.Value + }; +} + diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/CloudEventSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/CloudEventSurrogate.cs new file mode 100644 index 000000000000..7572ec3c31a3 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/CloudEventSurrogate.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// CloudEventSurrogate.cs + +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +// TODO: Add the rest of the properties +[GenerateSerializer] +public struct CloudEventSurrogate +{ + [Id(0)] + public string Id; + [Id(1)] + public string TextData; + [Id(2)] + public ByteString BinaryData; + [Id(3)] + public Any ProtoData; +} + +[RegisterConverter] +public sealed class CloudEventSurrogateConverter : + IConverter +{ + public CloudEvent ConvertFromSurrogate( + in CloudEventSurrogate surrogate) => + new CloudEvent + { + TextData = surrogate.TextData, + BinaryData = surrogate.BinaryData, + Id = surrogate.Id + }; + + public CloudEventSurrogate ConvertToSurrogate( + in CloudEvent value) => + new CloudEventSurrogate + { + TextData = value.TextData, + BinaryData = value.BinaryData, + Id = value.Id, + ProtoData = value.ProtoData + }; +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RegisterAgentTypeRequestSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RegisterAgentTypeRequestSurrogate.cs new file mode 100644 index 000000000000..5d8b6fd25a03 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RegisterAgentTypeRequestSurrogate.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RegisterAgentTypeRequestSurrogate.cs + +using Google.Protobuf.Collections; +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct RegisterAgentTypeRequestSurrogate +{ + [Id(0)] + public string RequestId; + [Id(1)] + public string Type; + [Id(2)] + public RepeatedField Events; + [Id(3)] + public RepeatedField Topics; +} + +[RegisterConverter] +public sealed class RegisterAgentTypeRequestSurrogateConverter : + IConverter +{ + public RegisterAgentTypeRequest ConvertFromSurrogate( + in RegisterAgentTypeRequestSurrogate surrogate) + { + var request = new RegisterAgentTypeRequest() + { + RequestId = surrogate.RequestId, + Type = surrogate.Type + }; + request.Events.Add(surrogate.Events); + request.Topics.Add(surrogate.Topics); + return request; + } + + public RegisterAgentTypeRequestSurrogate ConvertToSurrogate( + in RegisterAgentTypeRequest value) => + new RegisterAgentTypeRequestSurrogate + { + RequestId = value.RequestId, + Type = value.Type, + Events = value.Events, + Topics = value.Topics + }; +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RegisterAgentTypeResponseSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RegisterAgentTypeResponseSurrogate.cs new file mode 100644 index 000000000000..6fa73e5e8c3e --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RegisterAgentTypeResponseSurrogate.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RegisterAgentTypeResponseSurrogate.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct RegisterAgentTypeResponseSurrogate +{ + [Id(0)] + public string RequestId; + [Id(1)] + public bool Success; + [Id(2)] + public string Error; +} + +[RegisterConverter] +public sealed class RegisterAgentTypeResponseSurrogateConverter : + IConverter +{ + public RegisterAgentTypeResponse ConvertFromSurrogate( + in RegisterAgentTypeResponseSurrogate surrogate) => + new RegisterAgentTypeResponse + { + RequestId = surrogate.RequestId, + Success = surrogate.Success, + Error = surrogate.Error + }; + + public RegisterAgentTypeResponseSurrogate ConvertToSurrogate( + in RegisterAgentTypeResponse value) => + new RegisterAgentTypeResponseSurrogate + { + RequestId = value.RequestId, + Success = value.Success, + Error = value.Error + }; +} + diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RpcRequestSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RpcRequestSurrogate.cs new file mode 100644 index 000000000000..9791a68d7952 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RpcRequestSurrogate.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RpcRequestSurrogate.cs + +using Google.Protobuf.Collections; +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct RpcRequestSurrogate +{ + [Id(0)] + public string RequestId; + [Id(1)] + public AgentId Source; + [Id(2)] + public AgentId Target; + [Id(3)] + public string Method; + [Id(4)] + public Payload Payload; + [Id(5)] + public MapField Metadata; +} + +[RegisterConverter] +public sealed class RpcRequestSurrogateConverter : + IConverter +{ + public RpcRequest ConvertFromSurrogate( + in RpcRequestSurrogate surrogate) => + new RpcRequest + { + RequestId = surrogate.RequestId, + Source = surrogate.Source, + Target = surrogate.Target, + Method = surrogate.Method, + Payload = surrogate.Payload, + Metadata = { surrogate.Metadata } + }; + + public RpcRequestSurrogate ConvertToSurrogate( + in RpcRequest value) => + new RpcRequestSurrogate + { + RequestId = value.RequestId, + Source = value.Source, + Target = value.Target, + Method = value.Method, + Payload = value.Payload, + Metadata = value.Metadata + }; +} + diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RpcResponseSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RpcResponseSurrogate.cs new file mode 100644 index 000000000000..e5f9fff66405 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/RpcResponseSurrogate.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RpcResponseSurrogate.cs + +using Google.Protobuf.Collections; +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct RpcResponseSurrogate +{ + [Id(0)] + public string RequestId; + [Id(1)] + public Payload Payload; + [Id(2)] + public string Error; + [Id(3)] + public MapField Metadata; +} + +[RegisterConverter] +public sealed class RpcResponseurrogateConverter : + IConverter +{ + public RpcResponse ConvertFromSurrogate( + in RpcResponseSurrogate surrogate) => + new RpcResponse + { + RequestId = surrogate.RequestId, + Payload = surrogate.Payload, + Error = surrogate.Error, + Metadata = { surrogate.Metadata } + }; + + public RpcResponseSurrogate ConvertToSurrogate( + in RpcResponse value) => + new RpcResponseSurrogate + { + RequestId = value.RequestId, + Payload = value.Payload, + Error = value.Error, + Metadata = value.Metadata + }; +} + diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/SubscriptionRequestSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/SubscriptionRequestSurrogate.cs new file mode 100644 index 000000000000..f273b0b2347c --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/SubscriptionRequestSurrogate.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SubscriptionRequestSurrogate.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct SubscriptionRequestSurrogate +{ + [Id(0)] + public string RequestId; + [Id(1)] + public Subscription Subscription; +} + +[RegisterConverter] +public sealed class SubscriptionRequestSurrogateConverter : + IConverter +{ + public SubscriptionRequest ConvertFromSurrogate( + in SubscriptionRequestSurrogate surrogate) + { + var request = new SubscriptionRequest() + { + RequestId = surrogate.RequestId, + Subscription = surrogate.Subscription + }; + return request; + } + + public SubscriptionRequestSurrogate ConvertToSurrogate( + in SubscriptionRequest value) => + new SubscriptionRequestSurrogate + { + RequestId = value.RequestId, + Subscription = value.Subscription + }; +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/SubscriptionResponseSurrogate.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/SubscriptionResponseSurrogate.cs new file mode 100644 index 000000000000..5e8938643dcc --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/SubscriptionResponseSurrogate.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SubscriptionResponseSurrogate.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct SubscriptionResponseSurrogate +{ + [Id(0)] + public string RequestId; + [Id(1)] + public bool Success; + [Id(2)] + public string Error; +} + +[RegisterConverter] +public sealed class SubscriptionResponseSurrogateConverter : + IConverter +{ + public SubscriptionResponse ConvertFromSurrogate( + in SubscriptionResponseSurrogate surrogate) => + new SubscriptionResponse + { + RequestId = surrogate.RequestId, + Success = surrogate.Success, + Error = surrogate.Error + }; + + public SubscriptionResponseSurrogate ConvertToSurrogate( + in SubscriptionResponse value) => + new SubscriptionResponseSurrogate + { + RequestId = value.RequestId, + Success = value.Success, + Error = value.Error + }; +} + diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/TypePrefixSubscription.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/TypePrefixSubscription.cs new file mode 100644 index 000000000000..c5427d7bf972 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/TypePrefixSubscription.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TypePrefixSubscription.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct TypePrefixSubscriptionSurrogate +{ + [Id(0)] + public string TopicTypePrefix; + [Id(1)] + public string AgentType; +} + +[RegisterConverter] +public sealed class TypePrefixSubscriptionConverter : + IConverter +{ + public TypePrefixSubscription ConvertFromSurrogate( + in TypePrefixSubscriptionSurrogate surrogate) => + new TypePrefixSubscription + { + TopicTypePrefix = surrogate.TopicTypePrefix, + AgentType = surrogate.AgentType + }; + + public TypePrefixSubscriptionSurrogate ConvertToSurrogate( + in TypePrefixSubscription value) => + new TypePrefixSubscriptionSurrogate + { + TopicTypePrefix = value.TopicTypePrefix, + AgentType = value.AgentType + }; +} diff --git a/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/TypeSubscription.cs b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/TypeSubscription.cs new file mode 100644 index 000000000000..df38462d044a --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Runtime.Grpc/Services/Orleans/Surrogates/TypeSubscription.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TypeSubscription.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates; + +[GenerateSerializer] +public struct TypeSubscriptionSurrogate +{ + [Id(0)] + public string TopicType; + [Id(1)] + public string AgentType; +} + +[RegisterConverter] +public sealed class TypeSubscriptionSurrogateConverter : + IConverter +{ + public TypeSubscription ConvertFromSurrogate( + in TypeSubscriptionSurrogate surrogate) => + new TypeSubscription + { + TopicType = surrogate.TopicType, + AgentType = surrogate.AgentType + }; + + public TypeSubscriptionSurrogate ConvertToSurrogate( + in TypeSubscription value) => + new TypeSubscriptionSurrogate + { + TopicType = value.TopicType, + AgentType = value.AgentType + }; +} diff --git a/dotnet/test/Microsoft.AutoGen.Agents.Tests/AgentTests.cs b/dotnet/test/Microsoft.AutoGen.Agents.Tests/AgentTests.cs deleted file mode 100644 index 08a88da048de..000000000000 --- a/dotnet/test/Microsoft.AutoGen.Agents.Tests/AgentTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// AgentTests.cs - -using System.Collections.Concurrent; -using FluentAssertions; -using Google.Protobuf.Reflection; -using Microsoft.AspNetCore.Builder; -using Microsoft.AutoGen.Contracts; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; -using static Microsoft.AutoGen.Core.Tests.AgentTests; - -namespace Microsoft.AutoGen.Core.Tests; - -[Collection(ClusterFixtureCollection.Name)] -public class AgentTests(InMemoryAgentRuntimeFixture fixture) -{ - private readonly InMemoryAgentRuntimeFixture _fixture = fixture; - - [Fact] - public async Task ItInvokeRightHandlerTestAsync() - { - var mockWorker = new Mock(); - var agent = new TestAgent(mockWorker.Object, new EventTypes(TypeRegistry.Empty, [], []), new Logger(new LoggerFactory())); - - await agent.HandleObject("hello world"); - await agent.HandleObject(42); - - agent.ReceivedItems.Should().HaveCount(2); - agent.ReceivedItems[0].Should().Be("hello world"); - agent.ReceivedItems[1].Should().Be(42); - } - - [Fact] - public async Task ItDelegateMessageToTestAgentAsync() - { - var client = _fixture.AppHost.Services.GetRequiredService(); - - await client.PublishMessageAsync(new TextMessage() - { - Source = nameof(ItDelegateMessageToTestAgentAsync), - TextMessage_ = "buffer" - }, token: CancellationToken.None); - - // wait for 10 seconds - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - while (!TestAgent.ReceivedMessages.ContainsKey(nameof(ItDelegateMessageToTestAgentAsync)) && !cts.Token.IsCancellationRequested) - { - await Task.Delay(100); - } - - TestAgent.ReceivedMessages[nameof(ItDelegateMessageToTestAgentAsync)].Should().NotBeNull(); - } - - /// - /// The test agent is a simple agent that is used for testing purposes. - /// - public class TestAgent : Agent, IHandle, IHandle, IHandle - { - public TestAgent( - IAgentWorker worker, - [FromKeyedServices("EventTypes")] EventTypes eventTypes, - Logger? logger = null) : base(worker, eventTypes, logger) - { - } - - public Task Handle(string item) - { - ReceivedItems.Add(item); - return Task.CompletedTask; - } - - public Task Handle(int item) - { - ReceivedItems.Add(item); - return Task.CompletedTask; - } - - public Task Handle(TextMessage item) - { - ReceivedMessages[item.Source] = item.TextMessage_; - return Task.CompletedTask; - } - - public List ReceivedItems { get; private set; } = []; - - /// - /// Key: source - /// Value: message - /// - public static ConcurrentDictionary ReceivedMessages { get; private set; } = new(); - } -} - -public sealed class InMemoryAgentRuntimeFixture : IDisposable -{ - public InMemoryAgentRuntimeFixture() - { - var builder = WebApplication.CreateBuilder(); - - // step 1: create in-memory agent runtime - // step 2: register TestAgent to that agent runtime - builder - .AddAgentWorker() - .AddAgent(nameof(TestAgent)); - - AppHost = builder.Build(); - AppHost.StartAsync().Wait(); - } - public IHost AppHost { get; } - - void IDisposable.Dispose() - { - AppHost.StopAsync().Wait(); - AppHost.Dispose(); - } -} - -[CollectionDefinition(Name)] -public sealed class ClusterFixtureCollection : ICollectionFixture -{ - public const string Name = nameof(ClusterFixtureCollection); -} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs new file mode 100644 index 000000000000..f6005d1c8053 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentGrpcTests.cs + +using System.Collections.Concurrent; +using System.Text.Json; +using FluentAssertions; +using Google.Protobuf.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AutoGen.Contracts; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using static Microsoft.AutoGen.Core.Grpc.Tests.AgentGrpcTests; + +namespace Microsoft.AutoGen.Core.Grpc.Tests; + +[Collection(GrpcClusterFixtureCollection.Name)] +public class AgentGrpcTests(GrpcRuntimeFixture fixture) +{ + private readonly GrpcRuntimeFixture _fixture = fixture; + // need a variable to store the runtime instance + public static WebApplication? Host { get; private set; } + + /// + /// Verify that if the agent is not initialized via AgentWorker, it should throw the correct exception. + /// + /// void + [Fact] + public async Task Agent_ShouldThrowException_WhenNotInitialized() + { + var agent = ActivatorUtilities.CreateInstance(_fixture.Client.Services); + await Assert.ThrowsAsync( + async () => + { + await agent.SubscribeAsync("TestEvent"); + } + ); + } + + /// + /// validate that the agent is initialized correctly with implicit subs + /// + /// void + [Fact] + public async Task Agent_ShouldInitializeCorrectly() + { + var (worker, agent) = _fixture.Start(); + Assert.Equal("GrpcAgentWorker", worker.GetType().Name); + var subscriptions = await agent.GetSubscriptionsAsync(); + Assert.Equal(2, subscriptions.Count); + _fixture.Stop(); + } + /// + /// Test SubscribeAsync method + /// + /// void + [Fact] + public async Task SubscribeAsync_UnsubscribeAsync_and_GetSubscriptionsTest() + { + var (_, agent) = _fixture.Start(); + await agent.SubscribeAsync("TestEvent"); + await Task.Delay(100); + var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + var found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == "TestEvent") + { + found = true; + } + } + Assert.True(found); + await agent.UnsubscribeAsync("TestEvent").ConfigureAwait(true); + await Task.Delay(500); + subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == "TestEvent") + { + found = true; + } + } + Assert.False(found); + _fixture.Stop(); + } + + /// + /// Test StoreAsync and ReadAsync methods + /// + /// void + [Fact] + public async Task StoreAsync_and_ReadAsyncTest() + { + var (_, agent) = _fixture.Start(); + Dictionary state = new() + { + { "testdata", "Active" } + }; + await agent.StoreAsync(new AgentState + { + AgentId = agent.AgentId, + TextData = JsonSerializer.Serialize(state) + }).ConfigureAwait(true); + var readState = await agent.ReadAsync(agent.AgentId).ConfigureAwait(true); + var read = JsonSerializer.Deserialize>(readState.TextData) ?? new Dictionary { { "data", "No state data found" } }; + read.TryGetValue("testdata", out var value); + Assert.Equal("Active", value); + _fixture.Stop(); + } + + /// + /// Test PublishMessageAsync method and ReceiveMessage method + /// + /// void + [Fact] + public async Task PublishMessageAsync_and_ReceiveMessageTest() + { + var (_, agent) = _fixture.Start(); + await agent.SubscribeAsync("TestEvent").ConfigureAwait(true); + var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + var found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == "TestEvent") + { + found = true; + } + } + Assert.True(found); + + await agent.PublishMessageAsync(new TextMessage() + { + Source = "TestEvent", + TextMessage_ = "buffer" + }).ConfigureAwait(true); + await Task.Delay(10000); + Assert.True(TestAgent.ReceivedMessages.ContainsKey("TestEvent")); + _fixture.Stop(); + } + + [Fact] + public async Task InvokeCorrectHandler() + { + var agent = new TestAgent(new AgentsMetadata(TypeRegistry.Empty, new Dictionary(), new Dictionary>(), new Dictionary>()), new Logger(new LoggerFactory())); + + await agent.HandleObjectAsync("hello world"); + await agent.HandleObjectAsync(42); + + agent.ReceivedItems.Should().HaveCount(2); + agent.ReceivedItems[0].Should().Be("hello world"); + agent.ReceivedItems[1].Should().Be(42); + } + + [Fact] + public async Task DelegateMessageToTestAgentAsync() + { + var client = _fixture.Client.Services.GetRequiredService(); + await client.PublishMessageAsync(new TextMessage() + { + Source = nameof(DelegateMessageToTestAgentAsync), + TextMessage_ = "buffer" + }, token: CancellationToken.None); + + // wait for 10 seconds + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + while (!TestAgent.ReceivedMessages.ContainsKey(nameof(DelegateMessageToTestAgentAsync)) && !cts.Token.IsCancellationRequested) + { + await Task.Delay(100); + } + + TestAgent.ReceivedMessages[nameof(DelegateMessageToTestAgentAsync)].Should().NotBeNull(); + } + + /// + /// The test agent is a simple agent that is used for testing purposes. + /// + public class TestAgent( + [FromKeyedServices("AgentsMetadata")] AgentsMetadata eventTypes, + Logger? logger = null) : Agent(eventTypes, logger), IHandle + { + public Task Handle(TextMessage item, CancellationToken cancellationToken = default) + { + ReceivedMessages[item.Source] = item.TextMessage_; + return Task.CompletedTask; + } + public Task Handle(string item) + { + ReceivedItems.Add(item); + return Task.CompletedTask; + } + public Task Handle(int item) + { + ReceivedItems.Add(item); + return Task.CompletedTask; + } + public List ReceivedItems { get; private set; } = []; + + /// + /// Key: source + /// Value: message + /// + public static ConcurrentDictionary ReceivedMessages { get; private set; } = new(); + } +} + +/// +/// GrpcRuntimeFixture - provides a fixture for the agent runtime. +/// +/// +/// This fixture is used to provide a runtime for the agent tests. +/// However, it is shared between tests. So operations from one test can affect another. +/// +public sealed class GrpcRuntimeFixture +{ + public GrpcRuntimeFixture() + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + Environment.SetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS", "53071"); + Environment.SetEnvironmentVariable("AGENT_HOST", "https://localhost:53071"); + AppHost = StartAppHostAsync().GetAwaiter().GetResult(); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", "https://+;http://+"); + Client = StartClientAsync().GetAwaiter().GetResult(); + } + + private static async Task StartClientAsync() + { + return await AgentsApp.StartAsync().ConfigureAwait(false); + } + private static async Task StartAppHostAsync() + { + return await Microsoft.AutoGen.Runtime.Grpc.Host.StartAsync(local: false, useGrpc: true).ConfigureAwait(false); + + } + public IHost Client { get; } + public IHost? AppHost { get; } + + /// + /// Start - starts the agent + /// + /// IAgentWorker, TestAgent + public (IAgentWorker, TestAgent) Start() + { + var agent = ActivatorUtilities.CreateInstance(Client.Services); + var worker = Client.Services.GetRequiredService(); + Agent.Initialize(worker, agent); + return (worker, agent); + } + /// + /// Stop - stops the agent + /// + /// void + public void Stop() + { + IHostApplicationLifetime hostApplicationLifetime = Client.Services.GetRequiredService(); + hostApplicationLifetime.StopApplication(); + } +} + +[CollectionDefinition(Name)] +public sealed class GrpcClusterFixtureCollection : ICollectionFixture +{ + public const string Name = nameof(GrpcClusterFixtureCollection); +} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj new file mode 100644 index 000000000000..f14497e75fbc --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj @@ -0,0 +1,17 @@ + + + + $(TestTargetFrameworks) + enable + enable + True + + + + + + + + + + diff --git a/dotnet/samples/dev-team/DevTeam.AgentHost/Properties/launchSettings.json b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Properties/launchSettings.json similarity index 76% rename from dotnet/samples/dev-team/DevTeam.AgentHost/Properties/launchSettings.json rename to dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Properties/launchSettings.json index c43e7586ac17..cfddee319d65 100644 --- a/dotnet/samples/dev-team/DevTeam.AgentHost/Properties/launchSettings.json +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Properties/launchSettings.json @@ -1,12 +1,13 @@ { "profiles": { - "DevTeam.AgentHost": { + "AgentHost": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, + "applicationUrl": "https://localhost:50670;http://localhost:50673", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:50670;http://localhost:50673" + } } } -} \ No newline at end of file +} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/appsettings.json b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/appsettings.json new file mode 100644 index 000000000000..ae32fe371a70 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Orleans": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + } + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs new file mode 100644 index 000000000000..1e7b5ff10166 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentTests.cs + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Text.Json; +using FluentAssertions; +using Google.Protobuf.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AutoGen.Contracts; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using static Microsoft.AutoGen.Core.Tests.AgentTests; + +namespace Microsoft.AutoGen.Core.Tests; + +[Collection(ClusterFixtureCollection.Name)] +public class AgentTests(InMemoryAgentRuntimeFixture fixture) +{ + private readonly IServiceProvider _serviceProvider = fixture.AppHost.Services; + private readonly InMemoryAgentRuntimeFixture _fixture = fixture; + // need a variable to store the runtime instance + public static WebApplication? Host { get; private set; } + + /// + /// Verify that if the agent is not initialized via AgentWorker, it should throw the correct exception. + /// + /// void + [Fact] + public async Task Agent_ShouldThrowException_WhenNotInitialized() + { + var agent = ActivatorUtilities.CreateInstance(_serviceProvider); + await Assert.ThrowsAsync( + async () => + { + await agent.SubscribeAsync("TestEvent"); + } + ); + } + + /// + /// validate that the agent is initialized correctly with implicit subs + /// + /// void + [Fact] + public async Task Agent_ShouldInitializeCorrectly() + { + var (worker, agent) = _fixture.Start(); + Assert.Equal("AgentWorker", worker.GetType().Name); + var subscriptions = await agent.GetSubscriptionsAsync(); + Assert.Equal(2, subscriptions.Count); + _fixture.Stop(); + } + /// + /// Test SubscribeAsync method + /// + /// void + [Fact] + public async Task SubscribeAsync_UnsubscribeAsync_and_GetSubscriptionsTest() + { + var (_, agent) = _fixture.Start(); + await agent.SubscribeAsync("TestEvent"); + await Task.Delay(100); + var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + var found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == "TestEvent") + { + found = true; + } + } + Assert.True(found); + await agent.UnsubscribeAsync("TestEvent").ConfigureAwait(true); + await Task.Delay(500); + subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == "TestEvent") + { + found = true; + } + } + Assert.False(found); + _fixture.Stop(); + } + + /// + /// Test StoreAsync and ReadAsync methods + /// + /// void + [Fact] + public async Task StoreAsync_and_ReadAsyncTest() + { + var (_, agent) = _fixture.Start(); + Dictionary state = new() + { + { "testdata", "Active" } + }; + await agent.StoreAsync(new AgentState + { + AgentId = agent.AgentId, + TextData = JsonSerializer.Serialize(state) + }).ConfigureAwait(true); + var readState = await agent.ReadAsync(agent.AgentId).ConfigureAwait(true); + var read = JsonSerializer.Deserialize>(readState.TextData) ?? new Dictionary { { "data", "No state data found" } }; + read.TryGetValue("testdata", out var value); + Assert.Equal("Active", value); + _fixture.Stop(); + } + + /// + /// Test PublishMessageAsync method and ReceiveMessage method + /// + /// void + [Fact] + public async Task PublishMessageAsync_and_ReceiveMessageTest() + { + var (_, agent) = _fixture.Start(); + await agent.SubscribeAsync("TestEvent").ConfigureAwait(true); + var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + var found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == "TestEvent") + { + found = true; + } + } + Assert.True(found); + await agent.PublishMessageAsync(new TextMessage() + { + Source = "TestEvent", + TextMessage_ = "buffer" + }).ConfigureAwait(true); + await Task.Delay(100); + Assert.True(TestAgent.ReceivedMessages.ContainsKey("TestEvent")); + _fixture.Stop(); + } + + [Fact] + public async Task InvokeCorrectHandler() + { + var agent = new TestAgent(new AgentsMetadata(TypeRegistry.Empty, new Dictionary(), new Dictionary>(), new Dictionary>()), new Logger(new LoggerFactory())); + + await agent.HandleObjectAsync("hello world"); + await agent.HandleObjectAsync(42); + + agent.ReceivedItems.Should().HaveCount(2); + agent.ReceivedItems[0].Should().Be("hello world"); + agent.ReceivedItems[1].Should().Be(42); + } + + [Fact] + public async Task DelegateMessageToTestAgentAsync() + { + var client = _fixture.AppHost.Services.GetRequiredService(); + await client.PublishMessageAsync(new TextMessage() + { + Source = nameof(DelegateMessageToTestAgentAsync), + TextMessage_ = "buffer" + }, token: CancellationToken.None); + + // wait for 10 seconds + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + while (!TestAgent.ReceivedMessages.ContainsKey(nameof(DelegateMessageToTestAgentAsync)) && !cts.Token.IsCancellationRequested) + { + await Task.Delay(100); + } + + TestAgent.ReceivedMessages[nameof(DelegateMessageToTestAgentAsync)].Should().NotBeNull(); + } + + /// + /// The test agent is a simple agent that is used for testing purposes. + /// + public class TestAgent( + [FromKeyedServices("AgentsMetadata")] AgentsMetadata eventTypes, + Logger? logger = null) : Agent(eventTypes, logger), IHandle + { + public Task Handle(TextMessage item, CancellationToken cancellationToken = default) + { + ReceivedMessages[item.Source] = item.TextMessage_; + return Task.CompletedTask; + } + public Task Handle(string item) + { + ReceivedItems.Add(item); + return Task.CompletedTask; + } + public Task Handle(int item) + { + ReceivedItems.Add(item); + return Task.CompletedTask; + } + public List ReceivedItems { get; private set; } = []; + + /// + /// Key: source + /// Value: message + /// + public static ConcurrentDictionary ReceivedMessages { get; private set; } = new(); + } +} + +/// +/// InMemoryAgentRuntimeFixture - provides a fixture for the agent runtime. +/// +/// +/// This fixture is used to provide a runtime for the agent tests. +/// However, it is shared between tests. So operations from one test can affect another. +/// +public sealed class InMemoryAgentRuntimeFixture +{ + public InMemoryAgentRuntimeFixture() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.TryAddSingleton(DistributedContextPropagator.Current); + builder.AddAgentWorker() + .AddAgent(nameof(TestAgent)); + AppHost = builder.Build(); + AppHost.StartAsync().Wait(); + } + public IHost AppHost { get; } + + /// + /// Start - starts the agent + /// + /// IAgentWorker, TestAgent + public (IAgentWorker, TestAgent) Start() + { + var agent = ActivatorUtilities.CreateInstance(AppHost.Services); + var worker = AppHost.Services.GetRequiredService(); + Agent.Initialize(worker, agent); + return (worker, agent); + } + /// + /// Stop - stops the agent + /// + /// void + public void Stop() + { + IHostApplicationLifetime hostApplicationLifetime = AppHost.Services.GetRequiredService(); + hostApplicationLifetime.StopApplication(); + } +} + +[CollectionDefinition(Name)] +public sealed class ClusterFixtureCollection : ICollectionFixture +{ + public const string Name = nameof(ClusterFixtureCollection); +} diff --git a/dotnet/test/Microsoft.AutoGen.Agents.Tests/Microsoft.AutoGen.Agents.Tests.csproj b/dotnet/test/Microsoft.AutoGen.Core.Tests/Microsoft.AutoGen.Core.Tests.csproj similarity index 100% rename from dotnet/test/Microsoft.AutoGen.Agents.Tests/Microsoft.AutoGen.Agents.Tests.csproj rename to dotnet/test/Microsoft.AutoGen.Core.Tests/Microsoft.AutoGen.Core.Tests.csproj diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/GrpcGatewayServiceTests.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/GrpcGatewayServiceTests.cs new file mode 100644 index 000000000000..5be9404adc66 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/GrpcGatewayServiceTests.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GrpcGatewayServiceTests.cs + +using FluentAssertions; +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Core; +using Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Grpc; +using Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Orleans; +using Microsoft.Extensions.Logging; +using Moq; +using Tests.Events; +using NewMessageReceived = Tests.Events.NewMessageReceived; + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests; +[Collection(ClusterCollection.Name)] +public class GrpcGatewayServiceTests +{ + private readonly ClusterFixture _fixture; + + public GrpcGatewayServiceTests(ClusterFixture fixture) + { + _fixture = fixture; + } + // Test broadcast Event + [Fact] + public async Task Test_OpenChannel() + { + var logger = Mock.Of>(); + var gateway = new GrpcGateway(_fixture.Cluster.Client, logger); + var service = new GrpcGatewayService(gateway); + using var client = new TestGrpcClient(); + + gateway._workers.Count.Should().Be(0); + await service.OpenChannel(client.RequestStream, client.ResponseStream, client.CallContext); + gateway._workers.Count.Should().Be(1); + } + + [Fact] + public async Task Test_Message_Exchange_Through_Gateway() + { + var logger = Mock.Of>(); + var gateway = new GrpcGateway(_fixture.Cluster.Client, logger); + var service = new GrpcGatewayService(gateway); + using var client = new TestGrpcClient(); + + var assembly = typeof(PBAgent).Assembly; + var eventTypes = ReflectionHelper.GetAgentsMetadata(assembly); + + await service.OpenChannel(client.RequestStream, client.ResponseStream, client.CallContext); + + await service.RegisterAgent(CreateRegistrationRequest(eventTypes, typeof(PBAgent), client.CallContext.Peer), client.CallContext); + await service.RegisterAgent(CreateRegistrationRequest(eventTypes, typeof(GMAgent), client.CallContext.Peer), client.CallContext); + + var inputEvent = new NewMessageReceived { Message = $"Start-{client.CallContext.Peer}" }.ToCloudEvent("gh-gh-gh", "gh-gh-gh"); + + client.AddMessage(new Message { CloudEvent = inputEvent }); + var newMessageReceived = await client.ReadNext(); + newMessageReceived!.CloudEvent.Type.Should().Be(GetFullName(typeof(NewMessageReceived))); + newMessageReceived.CloudEvent.Source.Should().Be("gh-gh-gh"); + + // Simulate an agent, by publishing a new message in the request stream + var helloEvent = new Hello { Message = $"Hello test-{client.CallContext.Peer}" }.ToCloudEvent("gh-gh-gh", "gh-gh-gh"); + client.AddMessage(new Message { CloudEvent = helloEvent }); + + var helloMessageReceived = await client.ReadNext(); + helloMessageReceived!.CloudEvent.Type.Should().Be(GetFullName(typeof(Hello))); + helloMessageReceived.CloudEvent.Source.Should().Be("gh-gh-gh"); + } + + [Fact] + public async Task Test_Message_Goes_To_Right_Worker() + { + var logger = Mock.Of>(); + var gateway = new GrpcGateway(_fixture.Cluster.Client, logger); + var service = new GrpcGatewayService(gateway); + using var client = new TestGrpcClient(); + + var assembly = typeof(PBAgent).Assembly; + var eventTypes = ReflectionHelper.GetAgentsMetadata(assembly); + + await service.OpenChannel(client.RequestStream, client.ResponseStream, client.CallContext); + + await service.RegisterAgent(CreateRegistrationRequest(eventTypes, typeof(PBAgent), client.CallContext.Peer), client.CallContext); + await service.RegisterAgent(CreateRegistrationRequest(eventTypes, typeof(GMAgent), client.CallContext.Peer), client.CallContext); + + } + + [Fact] + public async Task Test_RegisterAgent_Should_Succeed() + { + var logger = Mock.Of>(); + var gateway = new GrpcGateway(_fixture.Cluster.Client, logger); + var service = new GrpcGatewayService(gateway); + using var client = new TestGrpcClient(); + + var assembly = typeof(PBAgent).Assembly; + var eventTypes = ReflectionHelper.GetAgentsMetadata(assembly); + + await service.OpenChannel(client.RequestStream, client.ResponseStream, client.CallContext); + + var response = await service.RegisterAgent(CreateRegistrationRequest(eventTypes, typeof(PBAgent), client.CallContext.Peer), client.CallContext); + response.Success.Should().BeTrue(); + } + + [Fact] + public async Task Test_RegisterAgent_Should_Fail_For_Wrong_ConnectionId() + { + var logger = Mock.Of>(); + var gateway = new GrpcGateway(_fixture.Cluster.Client, logger); + var service = new GrpcGatewayService(gateway); + using var client = new TestGrpcClient(); + + var assembly = typeof(PBAgent).Assembly; + var eventTypes = ReflectionHelper.GetAgentsMetadata(assembly); + + var response = await service.RegisterAgent(CreateRegistrationRequest(eventTypes, typeof(PBAgent), "faulty_connection_id"), client.CallContext); + response.Success.Should().BeFalse(); + } + + [Fact] + public async Task Test_SaveState() + { + var logger = Mock.Of>(); + var gateway = new GrpcGateway(_fixture.Cluster.Client, logger); + var service = new GrpcGatewayService(gateway); + var callContext = TestServerCallContext.Create(); + + var response = await service.SaveState(new AgentState { AgentId = new AgentId { Key = "", Type = "" } }, callContext); + + response.Should().NotBeNull(); + } + + [Fact] + public async Task Test_GetState() + { + var logger = Mock.Of>(); + var gateway = new GrpcGateway(_fixture.Cluster.Client, logger); + var service = new GrpcGatewayService(gateway); + var callContext = TestServerCallContext.Create(); + + var response = await service.GetState(new AgentId { Key = "", Type = "" }, callContext); + + response.Should().NotBeNull(); + } + + private RegisterAgentTypeRequest CreateRegistrationRequest(AgentsMetadata eventTypes, Type type, string requestId) + { + var registration = new RegisterAgentTypeRequest + { + Type = type.Name, + RequestId = requestId + }; + registration.Events.AddRange(eventTypes.GetEventsForAgent(type)?.ToList()); + registration.Topics.AddRange(eventTypes.GetTopicsForAgent(type)?.ToList()); + + return registration; + } + + private string GetFullName(Type type) + { + return ReflectionHelper.GetMessageDescriptor(type)!.FullName; + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestAsyncStreamReader.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestAsyncStreamReader.cs new file mode 100644 index 000000000000..4f26711d149f --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestAsyncStreamReader.cs @@ -0,0 +1,69 @@ +#pragma warning disable IDE0073 +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Threading.Channels; +using Grpc.Core; + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Grpc; + +public class TestAsyncStreamReader : IDisposable, IAsyncStreamReader + where T : class +{ + private readonly Channel _channel; + private readonly ServerCallContext _serverCallContext; + + public T Current { get; private set; } = null!; + + public TestAsyncStreamReader(ServerCallContext serverCallContext) + { + _channel = Channel.CreateUnbounded(); + _serverCallContext = serverCallContext; + } + + public void AddMessage(T message) + { + if (!_channel.Writer.TryWrite(message)) + { + throw new InvalidOperationException("Unable to write message."); + } + } + + public void Complete() + { + _channel.Writer.Complete(); + } + + public async Task MoveNext(CancellationToken cancellationToken) + { + _serverCallContext.CancellationToken.ThrowIfCancellationRequested(); + + if (await _channel.Reader.WaitToReadAsync(cancellationToken) && + _channel.Reader.TryRead(out var message)) + { + Current = message; + return true; + } + else + { + Current = null!; + return false; + } + } + + public void Dispose() + { + Complete(); + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestGrpcClient.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestGrpcClient.cs new file mode 100644 index 000000000000..6795c9f9237f --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestGrpcClient.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TestGrpcClient.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Grpc; +internal sealed class TestGrpcClient : IDisposable +{ + public TestAsyncStreamReader RequestStream { get; } + public TestServerStreamWriter ResponseStream { get; } + public TestServerCallContext CallContext { get; } + + public TestGrpcClient() + { + CallContext = TestServerCallContext.Create(); + RequestStream = new TestAsyncStreamReader(CallContext); + ResponseStream = new TestServerStreamWriter(CallContext); + } + + public async Task ReadNext() + { + var response = await ResponseStream.ReadNextAsync(); + return response!; + } + + public void AddMessage(Message message) + { + RequestStream.AddMessage(message); + } + + public void Dispose() + { + RequestStream.Dispose(); + ResponseStream.Dispose(); + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestServerCallContext.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestServerCallContext.cs new file mode 100644 index 000000000000..47f25155602d --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestServerCallContext.cs @@ -0,0 +1,73 @@ +#pragma warning disable IDE0073 +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Grpc.Core; + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Grpc; + +public class TestServerCallContext : ServerCallContext +{ + private readonly Metadata _requestHeaders; + private readonly CancellationToken _cancellationToken; + private readonly Metadata _responseTrailers; + private readonly AuthContext _authContext; + private readonly Dictionary _userState; + private WriteOptions? _writeOptions; + + public Metadata? ResponseHeaders { get; private set; } + + private TestServerCallContext(Metadata requestHeaders, CancellationToken cancellationToken) + { + _requestHeaders = requestHeaders; + _cancellationToken = cancellationToken; + _responseTrailers = new Metadata(); + _authContext = new AuthContext(string.Empty, new Dictionary>()); + _userState = new Dictionary(); + } + + protected override string MethodCore => "MethodName"; + protected override string HostCore => "HostName"; + protected override string PeerCore => "PeerName"; + protected override DateTime DeadlineCore { get; } + protected override Metadata RequestHeadersCore => _requestHeaders; + protected override CancellationToken CancellationTokenCore => _cancellationToken; + protected override Metadata ResponseTrailersCore => _responseTrailers; + protected override Status StatusCore { get; set; } + protected override WriteOptions? WriteOptionsCore { get => _writeOptions; set { _writeOptions = value; } } + protected override AuthContext AuthContextCore => _authContext; + + protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options) + { + throw new NotImplementedException(); + } + + protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) + { + if (ResponseHeaders != null) + { + throw new InvalidOperationException("Response headers have already been written."); + } + + ResponseHeaders = responseHeaders; + return Task.CompletedTask; + } + + protected override IDictionary UserStateCore => _userState; + + public static TestServerCallContext Create(Metadata? requestHeaders = null, CancellationToken cancellationToken = default) + { + return new TestServerCallContext(requestHeaders ?? new Metadata(), cancellationToken); + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestServerStreamWriter.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestServerStreamWriter.cs new file mode 100644 index 000000000000..ca2aeab2e410 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Grpc/TestServerStreamWriter.cs @@ -0,0 +1,86 @@ +#pragma warning disable IDE0073 +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Threading.Channels; +using Grpc.Core; + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Grpc; + +public class TestServerStreamWriter : IDisposable, IServerStreamWriter where T : class +{ + private readonly ServerCallContext _serverCallContext; + private readonly Channel _channel; + + public WriteOptions? WriteOptions { get; set; } + + public TestServerStreamWriter(ServerCallContext serverCallContext) + { + _channel = Channel.CreateUnbounded(); + + _serverCallContext = serverCallContext; + } + + public void Complete() + { + _channel.Writer.Complete(); + } + + public IAsyncEnumerable ReadAllAsync() + { + return _channel.Reader.ReadAllAsync(); + } + + public async Task ReadNextAsync() + { + if (await _channel.Reader.WaitToReadAsync()) + { + _channel.Reader.TryRead(out var message); + return message; + } + else + { + return null; + } + } + + public Task WriteAsync(T message, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + if (_serverCallContext.CancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(_serverCallContext.CancellationToken); + } + + if (!_channel.Writer.TryWrite(message)) + { + throw new InvalidOperationException("Unable to write message."); + } + + return Task.CompletedTask; + } + + public Task WriteAsync(T message) + { + return WriteAsync(message, CancellationToken.None); + } + + public void Dispose() + { + Complete(); + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/ClusterCollection.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/ClusterCollection.cs new file mode 100644 index 000000000000..d61dc7b21c50 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/ClusterCollection.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ClusterCollection.cs + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Orleans; + +[CollectionDefinition(Name)] +public sealed class ClusterCollection : ICollectionFixture +{ + public const string Name = nameof(ClusterCollection); +} diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/ClusterFixture.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/ClusterFixture.cs new file mode 100644 index 000000000000..9db2f7f654d4 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/ClusterFixture.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ClusterFixture.cs + +using Orleans.TestingHost; + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Orleans; + +public sealed class ClusterFixture : IDisposable +{ + public ClusterFixture() + { + var builder = new TestClusterBuilder(); + builder.AddSiloBuilderConfigurator(); + Cluster = builder.Build(); + Cluster.Deploy(); + + } + public TestCluster Cluster { get; } + + void IDisposable.Dispose() => Cluster.StopAllSilos(); +} diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/SiloBuilderConfigurator.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/SiloBuilderConfigurator.cs new file mode 100644 index 000000000000..fb345777a82b --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Helpers/Orleans/SiloBuilderConfigurator.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SiloBuilderConfigurator.cs + +using Orleans.Serialization; +using Orleans.TestingHost; + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests.Helpers.Orleans; + +public class SiloBuilderConfigurator : ISiloConfigurator +{ + public void Configure(ISiloBuilder siloBuilder) + { + siloBuilder.ConfigureServices(services => + { + services.AddSerializer(a => a.AddProtobufSerializer()); + }); + siloBuilder.AddMemoryStreams("StreamProvider") + .AddMemoryGrainStorage("PubSubStore") + .AddMemoryGrainStorage("AgentStateStore"); + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Microsoft.AutoGen.Runtime.Grpc.Tests.csproj b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Microsoft.AutoGen.Runtime.Grpc.Tests.csproj new file mode 100644 index 000000000000..23e3794606e1 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/Microsoft.AutoGen.Runtime.Grpc.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + True + + + + + + + + + + + + + + + + + + diff --git a/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/TestAgent.cs b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/TestAgent.cs new file mode 100644 index 000000000000..655a14cb418a --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Runtime.Grpc.Tests/TestAgent.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TestAgent.cs + +using System.Collections.Concurrent; +using Microsoft.AutoGen.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Tests.Events; + +namespace Microsoft.AutoGen.Runtime.Grpc.Tests; + +[TopicSubscription("gh-gh-gh")] +public class PBAgent([FromKeyedServices("AgentsMetadata")] AgentsMetadata eventTypes, ILogger? logger = null) + : Agent(eventTypes, logger) + , IHandle + , IHandle +{ + public async Task Handle(NewMessageReceived item, CancellationToken cancellationToken = default) + { + ReceivedMessages[AgentId.Key] = item.Message; + var hello = new Hello { Message = item.Message }; + await PublishMessageAsync(hello); + } + public Task Handle(GoodBye item, CancellationToken cancellationToken) + { + _logger.LogInformation($"Received GoodBye message {item.Message}"); + return Task.CompletedTask; + } + + public static ConcurrentDictionary ReceivedMessages { get; private set; } = new(); +} + +[TopicSubscription("gh-gh-gh")] +public class GMAgent([FromKeyedServices("AgentsMetadata")] AgentsMetadata eventTypes, ILogger? logger = null) + : Agent(eventTypes, logger) + , IHandle +{ + public async Task Handle(Hello item, CancellationToken cancellationToken) + { + _logger.LogInformation($"Received Hello message {item.Message}"); + ReceivedMessages[AgentId.Key] = item.Message; + await PublishMessageAsync(new GoodBye { Message = "" }); + } + + public static ConcurrentDictionary ReceivedMessages { get; private set; } = new(); +} diff --git a/dotnet/test/Microsoft.Autogen.Tests.Shared/Microsoft.Autogen.Tests.Shared.csproj b/dotnet/test/Microsoft.Autogen.Tests.Shared/Microsoft.Autogen.Tests.Shared.csproj new file mode 100644 index 000000000000..45b3dcc45309 --- /dev/null +++ b/dotnet/test/Microsoft.Autogen.Tests.Shared/Microsoft.Autogen.Tests.Shared.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + diff --git a/dotnet/test/Microsoft.Autogen.Tests.Shared/Protos/messages.proto b/dotnet/test/Microsoft.Autogen.Tests.Shared/Protos/messages.proto new file mode 100644 index 000000000000..cb68d45e7550 --- /dev/null +++ b/dotnet/test/Microsoft.Autogen.Tests.Shared/Protos/messages.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package tests; + +option csharp_namespace = "Tests.Events"; +message TextMessage { + string message = 1; + string source = 2; +} +message Hello { + string message = 1; +} +message InputProcessed { + string route = 1; +} +message Output { + string message = 1; +} +message OutputWritten { + string route = 1; +} +message IOError { + string message = 1; +} +message NewMessageReceived { + string message = 1; +} +message ResponseGenerated { + string response = 1; +} +message GoodBye { + string message = 1; +} +message MessageStored { + string message = 1; +} +message ConversationClosed { + string user_id = 1; + string user_message = 2; +} +message Shutdown { + string message = 1; +} diff --git a/protos/agent_worker.proto b/protos/agent_worker.proto index 7e658699b47e..543b3fba1321 100644 --- a/protos/agent_worker.proto +++ b/protos/agent_worker.proto @@ -50,6 +50,8 @@ message Event { message RegisterAgentTypeRequest { string request_id = 1; string type = 2; + repeated string events = 3; + repeated string topics = 4; } message RegisterAgentTypeResponse { @@ -75,12 +77,16 @@ message Subscription { } } -message AddSubscriptionRequest { +message SubscriptionList { + repeated Subscription subscriptions = 1; +} + +message SubscriptionRequest { string request_id = 1; Subscription subscription = 2; } -message AddSubscriptionResponse { +message SubscriptionResponse { string request_id = 1; bool success = 2; optional string error = 3; @@ -90,6 +96,10 @@ service AgentRpc { rpc OpenChannel (stream Message) returns (stream Message); rpc GetState(AgentId) returns (GetStateResponse); rpc SaveState(AgentState) returns (SaveStateResponse); + rpc RegisterAgent(RegisterAgentTypeRequest) returns (RegisterAgentTypeResponse); + rpc Subscribe(SubscriptionRequest) returns (SubscriptionResponse); + rpc Unsubscribe(SubscriptionRequest) returns (SubscriptionResponse); + rpc GetSubscriptions(AgentId) returns (SubscriptionList); } message AgentState { @@ -120,8 +130,8 @@ message Message { io.cloudevents.v1.CloudEvent cloudEvent = 3; RegisterAgentTypeRequest registerAgentTypeRequest = 4; RegisterAgentTypeResponse registerAgentTypeResponse = 5; - AddSubscriptionRequest addSubscriptionRequest = 6; - AddSubscriptionResponse addSubscriptionResponse = 7; + SubscriptionRequest SubscriptionRequest = 6; + SubscriptionResponse SubscriptionResponse = 7; } } diff --git a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/_worker_runtime.py b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/_worker_runtime.py index e764b0644191..b8c8375d819e 100644 --- a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/_worker_runtime.py +++ b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/_worker_runtime.py @@ -262,7 +262,7 @@ async def _run_read_loop(self) -> None: message = await self._host_connection.recv() # type: ignore oneofcase = agent_worker_pb2.Message.WhichOneof(message, "message") match oneofcase: - case "registerAgentTypeRequest" | "addSubscriptionRequest": + case "registerAgentTypeRequest" | "SubscriptionRequest": logger.warning(f"Cant handle {oneofcase}, skipping.") case "request": task = asyncio.create_task(self._process_request(message.request)) @@ -288,9 +288,9 @@ async def _run_read_loop(self) -> None: self._background_tasks.add(task) task.add_done_callback(self._raise_on_exception) task.add_done_callback(self._background_tasks.discard) - case "addSubscriptionResponse": + case "SubscriptionResponse": task = asyncio.create_task( - self._process_add_subscription_response(message.addSubscriptionResponse) + self._process_add_subscription_response(message.SubscriptionResponse) ) self._background_tasks.add(task) task.add_done_callback(self._raise_on_exception) @@ -809,7 +809,7 @@ async def add_subscription(self, subscription: Subscription) -> None: match subscription: case TypeSubscription(topic_type=topic_type, agent_type=agent_type): message = agent_worker_pb2.Message( - addSubscriptionRequest=agent_worker_pb2.AddSubscriptionRequest( + SubscriptionRequest=agent_worker_pb2.SubscriptionRequest( request_id=request_id, subscription=agent_worker_pb2.Subscription( typeSubscription=agent_worker_pb2.TypeSubscription( @@ -820,7 +820,7 @@ async def add_subscription(self, subscription: Subscription) -> None: ) case TypePrefixSubscription(topic_type_prefix=topic_type_prefix, agent_type=agent_type): message = agent_worker_pb2.Message( - addSubscriptionRequest=agent_worker_pb2.AddSubscriptionRequest( + SubscriptionRequest=agent_worker_pb2.SubscriptionRequest( request_id=request_id, subscription=agent_worker_pb2.Subscription( typePrefixSubscription=agent_worker_pb2.TypePrefixSubscription( @@ -844,7 +844,7 @@ async def add_subscription(self, subscription: Subscription) -> None: # Wait for the subscription response. await future - async def _process_add_subscription_response(self, response: agent_worker_pb2.AddSubscriptionResponse) -> None: + async def _process_add_subscription_response(self, response: agent_worker_pb2.SubscriptionResponse) -> None: future = self._pending_requests.pop(response.request_id) if response.HasField("error") and response.error != "": future.set_exception(RuntimeError(response.error)) diff --git a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/_worker_runtime_host_servicer.py b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/_worker_runtime_host_servicer.py index 0bb8ae0a8a20..67a43ef545b4 100644 --- a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/_worker_runtime_host_servicer.py +++ b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/_worker_runtime_host_servicer.py @@ -127,13 +127,13 @@ async def _receive_messages( self._background_tasks.add(task) task.add_done_callback(self._raise_on_exception) task.add_done_callback(self._background_tasks.discard) - case "addSubscriptionRequest": - add_subscription: agent_worker_pb2.AddSubscriptionRequest = message.addSubscriptionRequest + case "SubscriptionRequest": + add_subscription: agent_worker_pb2.SubscriptionRequest = message.SubscriptionRequest task = asyncio.create_task(self._process_add_subscription_request(add_subscription, client_id)) self._background_tasks.add(task) task.add_done_callback(self._raise_on_exception) task.add_done_callback(self._background_tasks.discard) - case "registerAgentTypeResponse" | "addSubscriptionResponse": + case "registerAgentTypeResponse" | "SubscriptionResponse": logger.warning(f"Received unexpected message type: {oneofcase}") case None: logger.warning("Received empty message") @@ -217,7 +217,7 @@ async def _process_register_agent_type_request( ) async def _process_add_subscription_request( - self, add_subscription_req: agent_worker_pb2.AddSubscriptionRequest, client_id: int + self, add_subscription_req: agent_worker_pb2.SubscriptionRequest, client_id: int ) -> None: oneofcase = add_subscription_req.subscription.WhichOneof("subscription") subscription: Subscription | None = None @@ -254,7 +254,7 @@ async def _process_add_subscription_request( # Send a response back to the client. await self._send_queues[client_id].put( agent_worker_pb2.Message( - addSubscriptionResponse=agent_worker_pb2.AddSubscriptionResponse( + SubscriptionResponse=agent_worker_pb2.SubscriptionResponse( request_id=add_subscription_req.request_id, success=success, error=error ) ) diff --git a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2.py b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2.py index b4794f1eaba6..76cd7f159b1a 100644 --- a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2.py +++ b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2.py @@ -16,7 +16,7 @@ from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x61gent_worker.proto\x12\x06\x61gents\x1a\x10\x63loudevent.proto\x1a\x19google/protobuf/any.proto\"\'\n\x07TopicId\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\"$\n\x07\x41gentId\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\"E\n\x07Payload\x12\x11\n\tdata_type\x18\x01 \x01(\t\x12\x19\n\x11\x64\x61ta_content_type\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"\x89\x02\n\nRpcRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12$\n\x06source\x18\x02 \x01(\x0b\x32\x0f.agents.AgentIdH\x00\x88\x01\x01\x12\x1f\n\x06target\x18\x03 \x01(\x0b\x32\x0f.agents.AgentId\x12\x0e\n\x06method\x18\x04 \x01(\t\x12 \n\x07payload\x18\x05 \x01(\x0b\x32\x0f.agents.Payload\x12\x32\n\x08metadata\x18\x06 \x03(\x0b\x32 .agents.RpcRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\t\n\x07_source\"\xb8\x01\n\x0bRpcResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12 \n\x07payload\x18\x02 \x01(\x0b\x32\x0f.agents.Payload\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\x33\n\x08metadata\x18\x04 \x03(\x0b\x32!.agents.RpcResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe4\x01\n\x05\x45vent\x12\x12\n\ntopic_type\x18\x01 \x01(\t\x12\x14\n\x0ctopic_source\x18\x02 \x01(\t\x12$\n\x06source\x18\x03 \x01(\x0b\x32\x0f.agents.AgentIdH\x00\x88\x01\x01\x12 \n\x07payload\x18\x04 \x01(\x0b\x32\x0f.agents.Payload\x12-\n\x08metadata\x18\x05 \x03(\x0b\x32\x1b.agents.Event.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\t\n\x07_source\"<\n\x18RegisterAgentTypeRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\"^\n\x19RegisterAgentTypeResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\":\n\x10TypeSubscription\x12\x12\n\ntopic_type\x18\x01 \x01(\t\x12\x12\n\nagent_type\x18\x02 \x01(\t\"G\n\x16TypePrefixSubscription\x12\x19\n\x11topic_type_prefix\x18\x01 \x01(\t\x12\x12\n\nagent_type\x18\x02 \x01(\t\"\x96\x01\n\x0cSubscription\x12\x34\n\x10typeSubscription\x18\x01 \x01(\x0b\x32\x18.agents.TypeSubscriptionH\x00\x12@\n\x16typePrefixSubscription\x18\x02 \x01(\x0b\x32\x1e.agents.TypePrefixSubscriptionH\x00\x42\x0e\n\x0csubscription\"X\n\x16\x41\x64\x64SubscriptionRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12*\n\x0csubscription\x18\x02 \x01(\x0b\x32\x14.agents.Subscription\"\\\n\x17\x41\x64\x64SubscriptionResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"\x9d\x01\n\nAgentState\x12!\n\x08\x61gent_id\x18\x01 \x01(\x0b\x32\x0f.agents.AgentId\x12\x0c\n\x04\x65Tag\x18\x02 \x01(\t\x12\x15\n\x0b\x62inary_data\x18\x03 \x01(\x0cH\x00\x12\x13\n\ttext_data\x18\x04 \x01(\tH\x00\x12*\n\nproto_data\x18\x05 \x01(\x0b\x32\x14.google.protobuf.AnyH\x00\x42\x06\n\x04\x64\x61ta\"j\n\x10GetStateResponse\x12\'\n\x0b\x61gent_state\x18\x01 \x01(\x0b\x32\x12.agents.AgentState\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"B\n\x11SaveStateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x12\n\x05\x65rror\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"\xad\x03\n\x07Message\x12%\n\x07request\x18\x01 \x01(\x0b\x32\x12.agents.RpcRequestH\x00\x12\'\n\x08response\x18\x02 \x01(\x0b\x32\x13.agents.RpcResponseH\x00\x12\x33\n\ncloudEvent\x18\x03 \x01(\x0b\x32\x1d.io.cloudevents.v1.CloudEventH\x00\x12\x44\n\x18registerAgentTypeRequest\x18\x04 \x01(\x0b\x32 .agents.RegisterAgentTypeRequestH\x00\x12\x46\n\x19registerAgentTypeResponse\x18\x05 \x01(\x0b\x32!.agents.RegisterAgentTypeResponseH\x00\x12@\n\x16\x61\x64\x64SubscriptionRequest\x18\x06 \x01(\x0b\x32\x1e.agents.AddSubscriptionRequestH\x00\x12\x42\n\x17\x61\x64\x64SubscriptionResponse\x18\x07 \x01(\x0b\x32\x1f.agents.AddSubscriptionResponseH\x00\x42\t\n\x07message2\xb2\x01\n\x08\x41gentRpc\x12\x33\n\x0bOpenChannel\x12\x0f.agents.Message\x1a\x0f.agents.Message(\x01\x30\x01\x12\x35\n\x08GetState\x12\x0f.agents.AgentId\x1a\x18.agents.GetStateResponse\x12:\n\tSaveState\x12\x12.agents.AgentState\x1a\x19.agents.SaveStateResponseB\x1e\xaa\x02\x1bMicrosoft.AutoGen.Contractsb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x61gent_worker.proto\x12\x06\x61gents\x1a\x10\x63loudevent.proto\x1a\x19google/protobuf/any.proto\"\'\n\x07TopicId\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\"$\n\x07\x41gentId\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\"E\n\x07Payload\x12\x11\n\tdata_type\x18\x01 \x01(\t\x12\x19\n\x11\x64\x61ta_content_type\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"\x89\x02\n\nRpcRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12$\n\x06source\x18\x02 \x01(\x0b\x32\x0f.agents.AgentIdH\x00\x88\x01\x01\x12\x1f\n\x06target\x18\x03 \x01(\x0b\x32\x0f.agents.AgentId\x12\x0e\n\x06method\x18\x04 \x01(\t\x12 \n\x07payload\x18\x05 \x01(\x0b\x32\x0f.agents.Payload\x12\x32\n\x08metadata\x18\x06 \x03(\x0b\x32 .agents.RpcRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\t\n\x07_source\"\xb8\x01\n\x0bRpcResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12 \n\x07payload\x18\x02 \x01(\x0b\x32\x0f.agents.Payload\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\x33\n\x08metadata\x18\x04 \x03(\x0b\x32!.agents.RpcResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe4\x01\n\x05\x45vent\x12\x12\n\ntopic_type\x18\x01 \x01(\t\x12\x14\n\x0ctopic_source\x18\x02 \x01(\t\x12$\n\x06source\x18\x03 \x01(\x0b\x32\x0f.agents.AgentIdH\x00\x88\x01\x01\x12 \n\x07payload\x18\x04 \x01(\x0b\x32\x0f.agents.Payload\x12-\n\x08metadata\x18\x05 \x03(\x0b\x32\x1b.agents.Event.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\t\n\x07_source\"\\\n\x18RegisterAgentTypeRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x0e\n\x06topics\x18\x04 \x03(\t\"^\n\x19RegisterAgentTypeResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\":\n\x10TypeSubscription\x12\x12\n\ntopic_type\x18\x01 \x01(\t\x12\x12\n\nagent_type\x18\x02 \x01(\t\"G\n\x16TypePrefixSubscription\x12\x19\n\x11topic_type_prefix\x18\x01 \x01(\t\x12\x12\n\nagent_type\x18\x02 \x01(\t\"\x96\x01\n\x0cSubscription\x12\x34\n\x10typeSubscription\x18\x01 \x01(\x0b\x32\x18.agents.TypeSubscriptionH\x00\x12@\n\x16typePrefixSubscription\x18\x02 \x01(\x0b\x32\x1e.agents.TypePrefixSubscriptionH\x00\x42\x0e\n\x0csubscription\"U\n\x13SubscriptionRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12*\n\x0csubscription\x18\x02 \x01(\x0b\x32\x14.agents.Subscription\"Y\n\x14SubscriptionResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"\x9d\x01\n\nAgentState\x12!\n\x08\x61gent_id\x18\x01 \x01(\x0b\x32\x0f.agents.AgentId\x12\x0c\n\x04\x65Tag\x18\x02 \x01(\t\x12\x15\n\x0b\x62inary_data\x18\x03 \x01(\x0cH\x00\x12\x13\n\ttext_data\x18\x04 \x01(\tH\x00\x12*\n\nproto_data\x18\x05 \x01(\x0b\x32\x14.google.protobuf.AnyH\x00\x42\x06\n\x04\x64\x61ta\"j\n\x10GetStateResponse\x12\'\n\x0b\x61gent_state\x18\x01 \x01(\x0b\x32\x12.agents.AgentState\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"B\n\x11SaveStateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x12\n\x05\x65rror\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"\xa1\x03\n\x07Message\x12%\n\x07request\x18\x01 \x01(\x0b\x32\x12.agents.RpcRequestH\x00\x12\'\n\x08response\x18\x02 \x01(\x0b\x32\x13.agents.RpcResponseH\x00\x12\x33\n\ncloudEvent\x18\x03 \x01(\x0b\x32\x1d.io.cloudevents.v1.CloudEventH\x00\x12\x44\n\x18registerAgentTypeRequest\x18\x04 \x01(\x0b\x32 .agents.RegisterAgentTypeRequestH\x00\x12\x46\n\x19registerAgentTypeResponse\x18\x05 \x01(\x0b\x32!.agents.RegisterAgentTypeResponseH\x00\x12:\n\x13SubscriptionRequest\x18\x06 \x01(\x0b\x32\x1b.agents.SubscriptionRequestH\x00\x12<\n\x14SubscriptionResponse\x18\x07 \x01(\x0b\x32\x1c.agents.SubscriptionResponseH\x00\x42\t\n\x07message2\xa7\x03\n\x08\x41gentRpc\x12\x33\n\x0bOpenChannel\x12\x0f.agents.Message\x1a\x0f.agents.Message(\x01\x30\x01\x12\x35\n\x08GetState\x12\x0f.agents.AgentId\x1a\x18.agents.GetStateResponse\x12:\n\tSaveState\x12\x12.agents.AgentState\x1a\x19.agents.SaveStateResponse\x12T\n\rRegisterAgent\x12 .agents.RegisterAgentTypeRequest\x1a!.agents.RegisterAgentTypeResponse\x12L\n\x0f\x41\x64\x64Subscription\x12\x1b.agents.SubscriptionRequest\x1a\x1c.agents.SubscriptionResponse\x12O\n\x12RemoveSubscription\x12\x1b.agents.SubscriptionRequest\x1a\x1c.agents.SubscriptionResponseB\x1e\xaa\x02\x1bMicrosoft.AutoGen.Contractsb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -49,27 +49,27 @@ _globals['_EVENT_METADATAENTRY']._serialized_start=433 _globals['_EVENT_METADATAENTRY']._serialized_end=480 _globals['_REGISTERAGENTTYPEREQUEST']._serialized_start=911 - _globals['_REGISTERAGENTTYPEREQUEST']._serialized_end=971 - _globals['_REGISTERAGENTTYPERESPONSE']._serialized_start=973 - _globals['_REGISTERAGENTTYPERESPONSE']._serialized_end=1067 - _globals['_TYPESUBSCRIPTION']._serialized_start=1069 - _globals['_TYPESUBSCRIPTION']._serialized_end=1127 - _globals['_TYPEPREFIXSUBSCRIPTION']._serialized_start=1129 - _globals['_TYPEPREFIXSUBSCRIPTION']._serialized_end=1200 - _globals['_SUBSCRIPTION']._serialized_start=1203 - _globals['_SUBSCRIPTION']._serialized_end=1353 - _globals['_ADDSUBSCRIPTIONREQUEST']._serialized_start=1355 - _globals['_ADDSUBSCRIPTIONREQUEST']._serialized_end=1443 - _globals['_ADDSUBSCRIPTIONRESPONSE']._serialized_start=1445 - _globals['_ADDSUBSCRIPTIONRESPONSE']._serialized_end=1537 - _globals['_AGENTSTATE']._serialized_start=1540 - _globals['_AGENTSTATE']._serialized_end=1697 - _globals['_GETSTATERESPONSE']._serialized_start=1699 - _globals['_GETSTATERESPONSE']._serialized_end=1805 - _globals['_SAVESTATERESPONSE']._serialized_start=1807 - _globals['_SAVESTATERESPONSE']._serialized_end=1873 - _globals['_MESSAGE']._serialized_start=1876 - _globals['_MESSAGE']._serialized_end=2305 - _globals['_AGENTRPC']._serialized_start=2308 - _globals['_AGENTRPC']._serialized_end=2486 + _globals['_REGISTERAGENTTYPEREQUEST']._serialized_end=1003 + _globals['_REGISTERAGENTTYPERESPONSE']._serialized_start=1005 + _globals['_REGISTERAGENTTYPERESPONSE']._serialized_end=1099 + _globals['_TYPESUBSCRIPTION']._serialized_start=1101 + _globals['_TYPESUBSCRIPTION']._serialized_end=1159 + _globals['_TYPEPREFIXSUBSCRIPTION']._serialized_start=1161 + _globals['_TYPEPREFIXSUBSCRIPTION']._serialized_end=1232 + _globals['_SUBSCRIPTION']._serialized_start=1235 + _globals['_SUBSCRIPTION']._serialized_end=1385 + _globals['_SUBSCRIPTIONREQUEST']._serialized_start=1387 + _globals['_SUBSCRIPTIONREQUEST']._serialized_end=1472 + _globals['_SUBSCRIPTIONRESPONSE']._serialized_start=1474 + _globals['_SUBSCRIPTIONRESPONSE']._serialized_end=1563 + _globals['_AGENTSTATE']._serialized_start=1566 + _globals['_AGENTSTATE']._serialized_end=1723 + _globals['_GETSTATERESPONSE']._serialized_start=1725 + _globals['_GETSTATERESPONSE']._serialized_end=1831 + _globals['_SAVESTATERESPONSE']._serialized_start=1833 + _globals['_SAVESTATERESPONSE']._serialized_end=1899 + _globals['_MESSAGE']._serialized_start=1902 + _globals['_MESSAGE']._serialized_end=2319 + _globals['_AGENTRPC']._serialized_start=2322 + _globals['_AGENTRPC']._serialized_end=2745 # @@protoc_insertion_point(module_scope) diff --git a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2.pyi b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2.pyi index 79e384ab948b..ff0ec2e9c801 100644 --- a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2.pyi +++ b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2.pyi @@ -220,15 +220,23 @@ class RegisterAgentTypeRequest(google.protobuf.message.Message): REQUEST_ID_FIELD_NUMBER: builtins.int TYPE_FIELD_NUMBER: builtins.int + EVENTS_FIELD_NUMBER: builtins.int + TOPICS_FIELD_NUMBER: builtins.int request_id: builtins.str type: builtins.str + @property + def events(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + @property + def topics(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... def __init__( self, *, request_id: builtins.str = ..., type: builtins.str = ..., + events: collections.abc.Iterable[builtins.str] | None = ..., + topics: collections.abc.Iterable[builtins.str] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing.Literal["request_id", b"request_id", "type", b"type"]) -> None: ... + def ClearField(self, field_name: typing.Literal["events", b"events", "request_id", b"request_id", "topics", b"topics", "type", b"type"]) -> None: ... global___RegisterAgentTypeRequest = RegisterAgentTypeRequest @@ -314,7 +322,7 @@ class Subscription(google.protobuf.message.Message): global___Subscription = Subscription @typing.final -class AddSubscriptionRequest(google.protobuf.message.Message): +class SubscriptionRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor REQUEST_ID_FIELD_NUMBER: builtins.int @@ -331,10 +339,10 @@ class AddSubscriptionRequest(google.protobuf.message.Message): def HasField(self, field_name: typing.Literal["subscription", b"subscription"]) -> builtins.bool: ... def ClearField(self, field_name: typing.Literal["request_id", b"request_id", "subscription", b"subscription"]) -> None: ... -global___AddSubscriptionRequest = AddSubscriptionRequest +global___SubscriptionRequest = SubscriptionRequest @typing.final -class AddSubscriptionResponse(google.protobuf.message.Message): +class SubscriptionResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor REQUEST_ID_FIELD_NUMBER: builtins.int @@ -354,7 +362,7 @@ class AddSubscriptionResponse(google.protobuf.message.Message): def ClearField(self, field_name: typing.Literal["_error", b"_error", "error", b"error", "request_id", b"request_id", "success", b"success"]) -> None: ... def WhichOneof(self, oneof_group: typing.Literal["_error", b"_error"]) -> typing.Literal["error"] | None: ... -global___AddSubscriptionResponse = AddSubscriptionResponse +global___SubscriptionResponse = SubscriptionResponse @typing.final class AgentState(google.protobuf.message.Message): @@ -440,8 +448,8 @@ class Message(google.protobuf.message.Message): CLOUDEVENT_FIELD_NUMBER: builtins.int REGISTERAGENTTYPEREQUEST_FIELD_NUMBER: builtins.int REGISTERAGENTTYPERESPONSE_FIELD_NUMBER: builtins.int - ADDSUBSCRIPTIONREQUEST_FIELD_NUMBER: builtins.int - ADDSUBSCRIPTIONRESPONSE_FIELD_NUMBER: builtins.int + SUBSCRIPTIONREQUEST_FIELD_NUMBER: builtins.int + SUBSCRIPTIONRESPONSE_FIELD_NUMBER: builtins.int @property def request(self) -> global___RpcRequest: ... @property @@ -453,9 +461,9 @@ class Message(google.protobuf.message.Message): @property def registerAgentTypeResponse(self) -> global___RegisterAgentTypeResponse: ... @property - def addSubscriptionRequest(self) -> global___AddSubscriptionRequest: ... + def SubscriptionRequest(self) -> global___SubscriptionRequest: ... @property - def addSubscriptionResponse(self) -> global___AddSubscriptionResponse: ... + def SubscriptionResponse(self) -> global___SubscriptionResponse: ... def __init__( self, *, @@ -464,11 +472,11 @@ class Message(google.protobuf.message.Message): cloudEvent: cloudevent_pb2.CloudEvent | None = ..., registerAgentTypeRequest: global___RegisterAgentTypeRequest | None = ..., registerAgentTypeResponse: global___RegisterAgentTypeResponse | None = ..., - addSubscriptionRequest: global___AddSubscriptionRequest | None = ..., - addSubscriptionResponse: global___AddSubscriptionResponse | None = ..., + SubscriptionRequest: global___SubscriptionRequest | None = ..., + SubscriptionResponse: global___SubscriptionResponse | None = ..., ) -> None: ... - def HasField(self, field_name: typing.Literal["addSubscriptionRequest", b"addSubscriptionRequest", "addSubscriptionResponse", b"addSubscriptionResponse", "cloudEvent", b"cloudEvent", "message", b"message", "registerAgentTypeRequest", b"registerAgentTypeRequest", "registerAgentTypeResponse", b"registerAgentTypeResponse", "request", b"request", "response", b"response"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["addSubscriptionRequest", b"addSubscriptionRequest", "addSubscriptionResponse", b"addSubscriptionResponse", "cloudEvent", b"cloudEvent", "message", b"message", "registerAgentTypeRequest", b"registerAgentTypeRequest", "registerAgentTypeResponse", b"registerAgentTypeResponse", "request", b"request", "response", b"response"]) -> None: ... - def WhichOneof(self, oneof_group: typing.Literal["message", b"message"]) -> typing.Literal["request", "response", "cloudEvent", "registerAgentTypeRequest", "registerAgentTypeResponse", "addSubscriptionRequest", "addSubscriptionResponse"] | None: ... + def HasField(self, field_name: typing.Literal["SubscriptionRequest", b"SubscriptionRequest", "SubscriptionResponse", b"SubscriptionResponse", "cloudEvent", b"cloudEvent", "message", b"message", "registerAgentTypeRequest", b"registerAgentTypeRequest", "registerAgentTypeResponse", b"registerAgentTypeResponse", "request", b"request", "response", b"response"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["SubscriptionRequest", b"SubscriptionRequest", "SubscriptionResponse", b"SubscriptionResponse", "cloudEvent", b"cloudEvent", "message", b"message", "registerAgentTypeRequest", b"registerAgentTypeRequest", "registerAgentTypeResponse", b"registerAgentTypeResponse", "request", b"request", "response", b"response"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["message", b"message"]) -> typing.Literal["request", "response", "cloudEvent", "registerAgentTypeRequest", "registerAgentTypeResponse", "SubscriptionRequest", "SubscriptionResponse"] | None: ... global___Message = Message diff --git a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2_grpc.py b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2_grpc.py index fc27021587f6..c363d9968034 100644 --- a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2_grpc.py +++ b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2_grpc.py @@ -29,6 +29,21 @@ def __init__(self, channel): request_serializer=agent__worker__pb2.AgentState.SerializeToString, response_deserializer=agent__worker__pb2.SaveStateResponse.FromString, ) + self.RegisterAgent = channel.unary_unary( + '/agents.AgentRpc/RegisterAgent', + request_serializer=agent__worker__pb2.RegisterAgentTypeRequest.SerializeToString, + response_deserializer=agent__worker__pb2.RegisterAgentTypeResponse.FromString, + ) + self.AddSubscription = channel.unary_unary( + '/agents.AgentRpc/AddSubscription', + request_serializer=agent__worker__pb2.SubscriptionRequest.SerializeToString, + response_deserializer=agent__worker__pb2.SubscriptionResponse.FromString, + ) + self.RemoveSubscription = channel.unary_unary( + '/agents.AgentRpc/RemoveSubscription', + request_serializer=agent__worker__pb2.SubscriptionRequest.SerializeToString, + response_deserializer=agent__worker__pb2.SubscriptionResponse.FromString, + ) class AgentRpcServicer(object): @@ -52,6 +67,24 @@ def SaveState(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def RegisterAgent(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def AddSubscription(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RemoveSubscription(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_AgentRpcServicer_to_server(servicer, server): rpc_method_handlers = { @@ -70,6 +103,21 @@ def add_AgentRpcServicer_to_server(servicer, server): request_deserializer=agent__worker__pb2.AgentState.FromString, response_serializer=agent__worker__pb2.SaveStateResponse.SerializeToString, ), + 'RegisterAgent': grpc.unary_unary_rpc_method_handler( + servicer.RegisterAgent, + request_deserializer=agent__worker__pb2.RegisterAgentTypeRequest.FromString, + response_serializer=agent__worker__pb2.RegisterAgentTypeResponse.SerializeToString, + ), + 'AddSubscription': grpc.unary_unary_rpc_method_handler( + servicer.AddSubscription, + request_deserializer=agent__worker__pb2.SubscriptionRequest.FromString, + response_serializer=agent__worker__pb2.SubscriptionResponse.SerializeToString, + ), + 'RemoveSubscription': grpc.unary_unary_rpc_method_handler( + servicer.RemoveSubscription, + request_deserializer=agent__worker__pb2.SubscriptionRequest.FromString, + response_serializer=agent__worker__pb2.SubscriptionResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'agents.AgentRpc', rpc_method_handlers) @@ -130,3 +178,54 @@ def SaveState(request, agent__worker__pb2.SaveStateResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def RegisterAgent(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/agents.AgentRpc/RegisterAgent', + agent__worker__pb2.RegisterAgentTypeRequest.SerializeToString, + agent__worker__pb2.RegisterAgentTypeResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def AddSubscription(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/agents.AgentRpc/AddSubscription', + agent__worker__pb2.SubscriptionRequest.SerializeToString, + agent__worker__pb2.SubscriptionResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def RemoveSubscription(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/agents.AgentRpc/RemoveSubscription', + agent__worker__pb2.SubscriptionRequest.SerializeToString, + agent__worker__pb2.SubscriptionResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2_grpc.pyi b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2_grpc.pyi index bf6bc1ba2d64..94539461f70c 100644 --- a/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2_grpc.pyi +++ b/python/packages/autogen-ext/src/autogen_ext/runtimes/grpc/protos/agent_worker_pb2_grpc.pyi @@ -34,6 +34,21 @@ class AgentRpcStub: agent_worker_pb2.SaveStateResponse, ] + RegisterAgent: grpc.UnaryUnaryMultiCallable[ + agent_worker_pb2.RegisterAgentTypeRequest, + agent_worker_pb2.RegisterAgentTypeResponse, + ] + + AddSubscription: grpc.UnaryUnaryMultiCallable[ + agent_worker_pb2.SubscriptionRequest, + agent_worker_pb2.SubscriptionResponse, + ] + + RemoveSubscription: grpc.UnaryUnaryMultiCallable[ + agent_worker_pb2.SubscriptionRequest, + agent_worker_pb2.SubscriptionResponse, + ] + class AgentRpcAsyncStub: OpenChannel: grpc.aio.StreamStreamMultiCallable[ agent_worker_pb2.Message, @@ -50,6 +65,21 @@ class AgentRpcAsyncStub: agent_worker_pb2.SaveStateResponse, ] + RegisterAgent: grpc.aio.UnaryUnaryMultiCallable[ + agent_worker_pb2.RegisterAgentTypeRequest, + agent_worker_pb2.RegisterAgentTypeResponse, + ] + + AddSubscription: grpc.aio.UnaryUnaryMultiCallable[ + agent_worker_pb2.SubscriptionRequest, + agent_worker_pb2.SubscriptionResponse, + ] + + RemoveSubscription: grpc.aio.UnaryUnaryMultiCallable[ + agent_worker_pb2.SubscriptionRequest, + agent_worker_pb2.SubscriptionResponse, + ] + class AgentRpcServicer(metaclass=abc.ABCMeta): @abc.abstractmethod def OpenChannel( @@ -72,4 +102,25 @@ class AgentRpcServicer(metaclass=abc.ABCMeta): context: _ServicerContext, ) -> typing.Union[agent_worker_pb2.SaveStateResponse, collections.abc.Awaitable[agent_worker_pb2.SaveStateResponse]]: ... + @abc.abstractmethod + def RegisterAgent( + self, + request: agent_worker_pb2.RegisterAgentTypeRequest, + context: _ServicerContext, + ) -> typing.Union[agent_worker_pb2.RegisterAgentTypeResponse, collections.abc.Awaitable[agent_worker_pb2.RegisterAgentTypeResponse]]: ... + + @abc.abstractmethod + def AddSubscription( + self, + request: agent_worker_pb2.SubscriptionRequest, + context: _ServicerContext, + ) -> typing.Union[agent_worker_pb2.SubscriptionResponse, collections.abc.Awaitable[agent_worker_pb2.SubscriptionResponse]]: ... + + @abc.abstractmethod + def RemoveSubscription( + self, + request: agent_worker_pb2.SubscriptionRequest, + context: _ServicerContext, + ) -> typing.Union[agent_worker_pb2.SubscriptionResponse, collections.abc.Awaitable[agent_worker_pb2.SubscriptionResponse]]: ... + def add_AgentRpcServicer_to_server(servicer: AgentRpcServicer, server: typing.Union[grpc.Server, grpc.aio.Server]) -> None: ...