rysweet-unsubscribe-and-agent-tests-4744 (#4920)

* add tests for core functionality and client/server
* add remove subscription, get subscriptions
* fix LOTS of bugs
* add grpc tuning
* adapt to latest agreed agents_worker proto changes.
This commit is contained in:
Ryan Sweet 2025-01-24 19:24:00 -08:00 committed by GitHub
parent 55e929db98
commit b6597fdd24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
132 changed files with 3799 additions and 1472 deletions

View File

@ -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}"
@ -140,7 +132,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Runtime.G
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Agents", "src\Microsoft.AutoGen\Agents\Microsoft.AutoGen.Agents.csproj", "{3892C83E-7F5D-41DF-A88C-4854EAD38856}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Autogen.AgentHost", "src\Microsoft.AutoGen\AgentHost\Microsoft.Autogen.AgentHost.csproj", "{4CB42139-DEE4-40B9-AA81-1E4CCAA2F338}"
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.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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Tests.Shared", "test\Microsoft.AutoGen.Tests.Shared\Microsoft.AutoGen.Tests.Shared.csproj", "{58AD8E1D-83BD-4950-A324-1A20677D78D9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -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,30 @@ 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
{58AD8E1D-83BD-4950-A324-1A20677D78D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58AD8E1D-83BD-4950-A324-1A20677D78D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58AD8E1D-83BD-4950-A324-1A20677D78D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58AD8E1D-83BD-4950-A324-1A20677D78D9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -417,11 +427,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 +436,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 +443,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}
{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}
{58AD8E1D-83BD-4950-A324-1A20677D78D9} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B}

View File

@ -45,6 +45,7 @@
<PackageVersion Include="MartinCostello.Logging.XUnit" Version="0.4.0" />
<PackageVersion Include="Microsoft.AspNetCore.App" Version="8.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.11" />
<PackageVersion Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsAIVersion)" />
@ -83,6 +84,7 @@
<PackageVersion Include="Microsoft.Orleans.Server" Version="$(MicrosoftOrleans)" />
<PackageVersion Include="Microsoft.Orleans.Streaming" Version="$(MicrosoftOrleans)" />
<PackageVersion Include="Microsoft.Orleans.Streaming.EventHubs" Version="$(MicrosoftOrleans)" />
<PackageVersion Include="Microsoft.Orleans.TestingHost" Version="9.0.1" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.29.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="$(MicrosoftSemanticKernelExperimentalVersion)" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureOpenAI" Version="1.29.0" />
@ -125,7 +127,6 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Microsoft.PowerShell.SDK" Version="7.4.5" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.11" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
</ItemGroup>
</Project>

View File

@ -18,7 +18,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../src/Microsoft.AutoGen/AgentHost/Microsoft.Autogen.AgentHost.csproj" />
<ProjectReference Include="../../../src/Microsoft.AutoGen/AgentHost/Microsoft.AutoGen.AgentHost.csproj" />
<ProjectReference Include="..\HelloAgent\HelloAgent.csproj" />
</ItemGroup>
</Project>

View File

@ -4,7 +4,7 @@
using Microsoft.Extensions.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var backend = builder.AddProject<Projects.Microsoft_Autogen_AgentHost>("backend").WithExternalHttpEndpoints();
var backend = builder.AddProject<Projects.Microsoft_AutoGen_AgentHost>("backend").WithExternalHttpEndpoints();
var client = builder.AddProject<Projects.HelloAgent>("HelloAgentsDotNET")
.WithReference(backend)
.WithEnvironment("AGENT_HOST", backend.GetEndpoint("https"))
@ -12,7 +12,8 @@ var client = builder.AddProject<Projects.HelloAgent>("HelloAgentsDotNET")
.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/samples/core_xlang_hello_python_agent", "hello_python_agent.py", "../../.venv").WithReference(backend)
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")
.WithEnvironment("GRPC_DNS_RESOLVER", "native")

View File

@ -8,17 +8,15 @@ using Microsoft.Extensions.AI;
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<NewMessageReceived>
{
// 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);

View File

@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Agents\Microsoft.AutoGen.Agents.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Contracts\Microsoft.AutoGen.Contracts.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Core\Microsoft.AutoGen.Core.csproj" />

View File

@ -18,31 +18,30 @@ if (Environment.GetEnvironmentVariable("AZURE_OPENAI_CONNECTION_STRING") == null
}
builder.Configuration["ConnectionStrings:HelloAIAgents"] = Environment.GetEnvironmentVariable("AZURE_OPENAI_CONNECTION_STRING");
builder.AddChatCompletionService("HelloAIAgents");
var agentTypes = new AgentTypes(new Dictionary<string, Type>
var _ = new AgentTypes(new Dictionary<string, Type>
{
{ "HelloAIAgents", typeof(HelloAIAgent) }
});
var app = await AgentsApp.PublishMessageAsync("HelloAgents", new NewMessageReceived
var local = true;
if (Environment.GetEnvironmentVariable("AGENT_HOST") != null) { local = false; }
var app = await Microsoft.AutoGen.Core.Grpc.AgentsApp.PublishMessageAsync("HelloAgents", new NewMessageReceived
{
Message = "World"
}, builder, agentTypes, local: true);
}, local: local).ConfigureAwait(false);
await app.WaitForShutdownAsync();
namespace Hello
{
[TopicSubscription("agents")]
[TopicSubscription("HelloAgents")]
public class HelloAgent(
IAgentWorker worker,
[FromKeyedServices("EventTypes")] EventTypes typeRegistry,
[FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry,
IHostApplicationLifetime hostApplicationLifetime) : ConsoleAgent(
worker,
typeRegistry),
ISayHello,
IHandle<NewMessageReceived>,
IHandle<ConversationClosed>
{
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 +56,7 @@ namespace Hello
};
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

View File

@ -18,9 +18,8 @@ namespace Hello
{
[TopicSubscription("HelloAgents")]
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 @@ namespace Hello
IHandle<ConversationClosed>,
IHandle<Shutdown>
{
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 @@ namespace Hello
};
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 @@ namespace Hello
}
}
public async Task Handle(Shutdown item)
public async Task Handle(Shutdown item, CancellationToken cancellationToken)
{
Console.WriteLine("Shutting down...");
hostApplicationLifetime.StopApplication();
}
public async Task<string> SayHello(string ask)
public async Task<string> SayHello(string ask, CancellationToken cancellationToken = default)
{
var response = $"\n\n\n\n***************Hello {ask}**********************\n\n\n\n";
return response;
@ -65,6 +64,6 @@ namespace Hello
}
public interface ISayHello
{
public Task<string> SayHello(string ask);
public Task<string> SayHello(string ask, CancellationToken cancellationToken = default);
}
}

View File

@ -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<NewMessageReceived>,
IHandle<ConversationClosed>
{
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

View File

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Agents\Microsoft.AutoGen.Agents.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Contracts\Microsoft.AutoGen.Contracts.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Core\Microsoft.AutoGen.Core.csproj" />

View File

@ -7,21 +7,21 @@ using Microsoft.AutoGen.Contracts;
using Microsoft.AutoGen.Core;
// send a message to the agent
var app = await AgentsApp.PublishMessageAsync("HelloAgents", new NewMessageReceived
var local = true;
if (Environment.GetEnvironmentVariable("AGENT_HOST") != null) { local = false; }
var app = await Microsoft.AutoGen.Core.Grpc.AgentsApp.PublishMessageAsync("HelloAgents", new NewMessageReceived
{
Message = "World"
}, local: false);
}, local: local).ConfigureAwait(false);
await app.WaitForShutdownAsync();
namespace Hello
{
[TopicSubscription("agents")]
[TopicSubscription("HelloAgents")]
public class HelloAgent(
IAgentWorker worker,
IHostApplicationLifetime hostApplicationLifetime,
[FromKeyedServices("EventTypes")] EventTypes typeRegistry) : Agent(
worker,
[FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry) : Agent(
typeRegistry),
IHandleConsole,
IHandle<NewMessageReceived>,
@ -29,7 +29,7 @@ namespace Hello
IHandle<Shutdown>
{
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 +57,7 @@ namespace Hello
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<AgentState>(this.AgentId).ConfigureAwait(false);
var state = JsonSerializer.Deserialize<Dictionary<string, string>>(State.TextData) ?? new Dictionary<string, string> { { "data", "No state data found" } };
@ -74,7 +74,7 @@ namespace Hello
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

View File

@ -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<NewMessageReceived>,
IHandle<ConversationClosed>
{
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

View File

@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!--orleans doesn't have strong name package-->
<NoWarn>$(NoWarn);CS8002</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../../src/Microsoft.AutoGen/Core/Microsoft.AutoGen.Core.csproj" />
<ProjectReference Include="../../../src/Microsoft.AutoGen/Runtime.Grpc/Microsoft.AutoGen.Runtime.Grpc.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Extensions\Aspire\Microsoft.AutoGen.Extensions.Aspire.csproj" />
</ItemGroup>
</Project>

View File

@ -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();

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../../src/Microsoft.AutoGen/Core/Microsoft.AutoGen.Core.csproj" />
<ProjectReference Include="../../../src/Microsoft.AutoGen/Agents/Microsoft.AutoGen.Agents.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Extensions\Aspire\Microsoft.AutoGen.Extensions.Aspire.csproj" />
<ProjectReference Include="..\DevTeam.Shared\DevTeam.Shared.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Extensions\SemanticKernel\Microsoft.AutoGen.Extensions.SemanticKernel.csproj" />
</ItemGroup>
</Project>

View File

@ -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<Dev> logger)
: SKAiAgent<DeveloperState>(worker, memory, kernel, typeRegistry), IDevelopApps,
IHandle<CodeGenerationRequested>,
IHandle<CodeChainClosed>
{
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<string> 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<string> GenerateCode(string ask);
}

View File

@ -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<DeveloperLead> logger)
: SKAiAgent<DeveloperLeadState>(worker, memory, kernel, typeRegistry), ILeadDevelopers,
IHandle<DevPlanRequested>,
IHandle<DevPlanChainClosed>
{
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<string> 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<string> CreatePlan(string ask);
}

View File

@ -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<ProductManager> logger)
: SKAiAgent<ProductManagerState>(worker, memory, kernel, typeRegistry), IManageProducts,
IHandle<ReadmeChainClosed>,
IHandle<ReadmeRequested>
{
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<string> 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<string> CreateReadme(string ask);
}

View File

@ -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<Dev>(nameof(Dev))
.AddAgent<ProductManager>(nameof(ProductManager))
.AddAgent<DeveloperLead>(nameof(DeveloperLead));
var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();

View File

@ -1,12 +0,0 @@
{
"profiles": {
"DevTeam.Agents": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:50669;http://localhost:50671"
}
}
}

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -21,8 +21,6 @@
<ItemGroup>
<ProjectReference Include="..\DevTeam.Backend\DevTeam.Backend.csproj" />
<ProjectReference Include="..\DevTeam.AgentHost\DevTeam.AgentHost.csproj" />
<ProjectReference Include="..\DevTeam.Agents\DevTeam.Agents.csproj" />
</ItemGroup>
</Project>

View File

@ -7,22 +7,16 @@ builder.AddAzureProvisioning();
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<Projects.DevTeam_AgentHost>("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<Projects.DevTeam_Backend>("backend")
.WithEnvironment("AGENT_HOST", $"{agentHostHttps.Property(EndpointProperty.Url)}")
.WithEnvironment("Qdrant__Endpoint", $"{qdrant.Resource.HttpEndpoint.Property(EndpointProperty.Url)}")
@ -33,16 +27,10 @@ builder.AddProject<Projects.DevTeam_Backend>("backend")
.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<Projects.DevTeam_Agents>("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();

View File

@ -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"
}
}
}
}

View File

@ -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<object>(worker, memory, kernel, typeRegistry),
[TopicSubscription(Consts.TopicName)]
public class AzureGenie([FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, IManageAzure azureService)
: Agent(typeRegistry),
IHandle<ReadmeCreated>,
IHandle<CodeCreated>
{
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 class AzureGenie(IAgentWorker worker, Kernel kernel, ISemanticTextMemory
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();

View File

@ -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<Dev> logger)
: AiAgent<DeveloperState>(typeRegistry, logger), IDevelopApps,
IHandle<CodeGenerationRequested>,
IHandle<CodeChainClosed>
{
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<string> 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<string> GenerateCode(string ask);
}

View File

@ -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 = """

View File

@ -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<DeveloperLead> logger)
: AiAgent<DeveloperLeadState>(typeRegistry, logger), ILeadDevelopers,
IHandle<DevPlanRequested>,
IHandle<DevPlanChainClosed>
{
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<string> 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<string> CreatePlan(string ask);
}

View File

@ -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 = """

View File

@ -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<object>(worker, memory, kernel, typeRegistry),
[TopicSubscription(Consts.TopicName)]
public class Hubber([FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry, IManageGithub ghService)
: Agent(typeRegistry),
IHandle<NewAsk>,
IHandle<ReadmeGenerated>,
IHandle<DevPlanGenerated>,
@ -21,7 +17,7 @@ public class Hubber(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memo
IHandle<ReadmeStored>,
IHandle<CodeGenerated>
{
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 class Hubber(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memo
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<DevLeadPlan>(item.Plan);
var prompts = plan!.Steps.SelectMany(s => s.Subtasks!.Select(st => st.Prompt));
@ -62,7 +58,7 @@ public class Hubber(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memo
}
}
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);

View File

@ -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 = """

View File

@ -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<ProductManager> logger)
: AiAgent<ProductManagerState>(typeRegistry, logger), IManageProducts,
IHandle<ReadmeChainClosed>,
IHandle<ReadmeRequested>
{
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<string> 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<string> CreateReadme(string ask);
}

View File

@ -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;

View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// AiAgent.cs
using Microsoft.AutoGen.Core;
namespace DevTeam.Agents;
public class AiAgent<T> : Agent
{
public AiAgent(AgentsMetadata eventTypes, ILogger<AiAgent<T>> logger) : base(eventTypes, logger)
{
}
protected async Task AddKnowledge(string instruction, string v)
{
throw new NotImplementedException();
}
protected async Task<string> CallFunction(string prompt)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Consts.cs
namespace DevTeam.Backend;
public class Consts
{
public const string TopicName = "devteam";
}

View File

@ -1,8 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="../../../src/Microsoft.AutoGen/Core/Microsoft.AutoGen.Core.csproj" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@ -12,11 +8,9 @@
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Octokit.Webhooks.AspNetCore" />
<PackageReference Include="Octokit" />
<PackageReference Include="Microsoft.SemanticKernel" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Memory" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Microsoft.Extensions.Azure" />
@ -26,13 +20,19 @@
<PackageReference Include="Azure.Data.Tables" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../src/Microsoft.AutoGen/Agents/Microsoft.AutoGen.Agents.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Extensions\Aspire\Microsoft.AutoGen.Extensions.Aspire.csproj" />
<ProjectReference Include="..\DevTeam.Shared\DevTeam.Shared.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Extensions\SemanticKernel\Microsoft.AutoGen.Extensions.SemanticKernel.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Core.Grpc\Microsoft.AutoGen.Core.Grpc.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AutoGen\Core\Microsoft.AutoGen.Core.csproj" />
<ProjectReference Include="..\DevTeam.ServiceDefaults\DevTeam.ServiceDefaults.csproj" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="..\Protos\messages.proto" Link="Protos\messages.proto" />
<Protobuf Include="..\Protos\states.proto" Link="Protos\states.proto" />
</ItemGroup>
</Project>

View File

@ -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 @@ using Octokit.Webhooks.AspNetCore;
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<AzureGenie>(nameof(AzureGenie))
//.AddAgent<Sandbox>(nameof(Sandbox))
.AddAgent<Hubber>(nameof(Hubber));
.AddAgent<Hubber>(nameof(Hubber))
.AddAgent<Dev>(nameof(Dev))
.AddAgent<ProductManager>(nameof(ProductManager))
.AddAgent<DeveloperLead>(nameof(DeveloperLead));
builder.Services.AddSingleton<AgentWorker>();
builder.Services.AddSingleton<WebhookEventProcessor, GithubWebHookProcessor>();
@ -58,7 +64,7 @@ builder.Services.AddAzureClients(clientBuilder =>
var app = builder.Build();
app.MapDefaultEndpoints();
Microsoft.Extensions.Hosting.AspireHostingExtensions.MapDefaultEndpoints(app);
app.UseRouting()
.UseEndpoints(endpoints =>
{

View File

@ -12,7 +12,7 @@ using Azure.Storage.Files.Shares;
using DevTeam.Options;
using Microsoft.Extensions.Options;
namespace DevTeam.Backend;
namespace DevTeam.Backend.Services;
public class AzureService : IManageAzure
{

View File

@ -9,7 +9,7 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Octokit;
namespace DevTeam.Backend;
namespace DevTeam.Backend.Services;
public class GithubAuthService
{
private readonly GithubOptions _githubSettings;

View File

@ -8,7 +8,7 @@ using Microsoft.Extensions.Options;
using Octokit;
using Octokit.Helpers;
namespace DevTeam.Backend;
namespace DevTeam.Backend.Services;
public class GithubService : IManageGithub
{

View File

@ -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.IssueComment;
using Octokit.Webhooks.Events.Issues;
using Octokit.Webhooks.Models;
namespace DevTeam.Backend;
namespace DevTeam.Backend.Services;
public sealed class GithubWebHookProcessor(ILogger<GithubWebHookProcessor> logger, AgentWorker client) : WebhookEventProcessor
public sealed class GithubWebHookProcessor(ILogger<GithubWebHookProcessor> logger, Client client) : WebhookEventProcessor
{
private readonly ILogger<GithubWebHookProcessor> _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 @@ public sealed class GithubWebHookProcessor(ILogger<GithubWebHookProcessor> logge
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 @@ public sealed class GithubWebHookProcessor(ILogger<GithubWebHookProcessor> logge
{
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 @@ public sealed class GithubWebHookProcessor(ILogger<GithubWebHookProcessor> logge
_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)
{

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>
</Project>

View File

@ -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<TBuilder>(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<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });
return builder;
}
public static TBuilder ConfigureOpenTelemetry<TBuilder>(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<TBuilder>(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<TBuilder>(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;
}
}

View File

@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="../../../src/Microsoft.AutoGen/Core/Microsoft.AutoGen.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Microsoft.SemanticKernel" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Memory" />
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="..\Protos\messages.proto" Link="Protos\messages.proto" />
<Protobuf Include="..\Protos\states.proto" Link="Protos\states.proto" />
</ItemGroup>
</Project>

View File

@ -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<string, string>();// JsonSerializer.Deserialize<Dictionary<string,string>>(evt.Data);
return new GithubContext
{
Org = data?["org"] ?? "",
Repo = data?["repo"] ?? "",
IssueNumber = data?.TryParseLong("issueNumber") ?? default,
ParentNumber = data?.TryParseLong("parentNumber")
};
}
public static Dictionary<string, string> ToData(this Event evt)
{
ArgumentNullException.ThrowIfNull(evt);
return //JsonSerializer.Deserialize<Dictionary<string,string>>(evt.Data) ??
new Dictionary<string, string>();
}
public static Dictionary<string, string> ToData(this GithubContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new Dictionary<string, string> {
{ "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}";
}

View File

@ -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<string, string> 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;
}
}

View File

@ -2,7 +2,7 @@ syntax = "proto3";
package devteam;
option csharp_namespace = "DevTeam.Shared";
option csharp_namespace = "DevTeam";
message NewAsk {
string org = 1;

View File

@ -2,7 +2,7 @@ syntax = "proto3";
package devteam;
option csharp_namespace = "DevTeam.Shared";
option csharp_namespace = "DevTeam";
message DeveloperState {

View File

@ -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

View File

@ -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"
}

View File

@ -2,9 +2,12 @@
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore": "Information",
"Microsoft": "Information",
"Microsoft.Orleans": "Warning",
"Orleans.Runtime": "Error"
"Orleans.Runtime": "Error",
"Grpc": "Information"
}
},
"AllowedHosts": "*",

View File

@ -5,10 +5,9 @@ using Microsoft.AutoGen.Core;
using Microsoft.Extensions.AI;
namespace Microsoft.AutoGen.Agents;
public abstract class InferenceAgent<T>(
IAgentWorker worker,
EventTypes typeRegistry,
AgentsMetadata typeRegistry,
IChatClient client)
: Agent(worker, typeRegistry)
: Agent(typeRegistry)
where T : IMessage, new()
{
protected IChatClient ChatClient { get; } = client;

View File

@ -9,11 +9,9 @@ using Microsoft.SemanticKernel.Memory;
namespace Microsoft.AutoGen.Agents;
public abstract class SKAiAgent<T>(
IAgentWorker worker,
ISemanticTextMemory memory,
Kernel kernel,
EventTypes typeRegistry) : Agent(
worker,
AgentsMetadata typeRegistry) : Agent(
typeRegistry) where T : class, new()
{
protected AgentState<T> _state = new();

View File

@ -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 abstract class ConsoleAgent : IOAgent,
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);

View File

@ -10,9 +10,9 @@ namespace Microsoft.AutoGen.Agents;
public interface IHandleConsole : IHandle<Output>, IHandle<Input>
{
AgentId AgentId { get; }
ValueTask PublishMessageAsync<T>(T message, string? source = null, CancellationToken token = default) where T : IMessage;
ValueTask PublishMessageAsync<T>(T message, CancellationToken token = default) where T : IMessage;
async Task IHandle<Output>.Handle(Output item)
async Task IHandle<Output>.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 @@ public interface IHandleConsole : IHandle<Output>, IHandle<Input>
{
Route = "console"
};
await PublishMessageAsync(evt);
await PublishMessageAsync(evt).ConfigureAwait(false);
}
async Task IHandle<Input>.Handle(Input item)
async Task IHandle<Input>.Handle(Input item, CancellationToken cancellationToken)
{
Console.WriteLine("Please enter input:");
string content = Console.ReadLine() ?? string.Empty;
@ -35,7 +35,7 @@ public interface IHandleConsole : IHandle<Output>, IHandle<Input>
{
Route = "console"
};
await PublishMessageAsync(evt);
await PublishMessageAsync(evt).ConfigureAwait(false);
}
static Task ProcessOutput(string message)
{

View File

@ -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<Input>,
IHandle<Output>
{
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 abstract class FileAgent(
};
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))
{

View File

@ -5,11 +5,11 @@ using Microsoft.AutoGen.Contracts;
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 abstract class IOAgent(IAgentWorker worker, EventTypes eventTypes) : Agen
await PublishMessageAsync(evt);
}
public virtual async Task Handle(Output item)
public virtual async Task Handle(Output item, CancellationToken cancellationToken)
{
var evt = new OutputWritten
{

View File

@ -18,10 +18,9 @@ public abstract class WebAPIAgent : IOAgent,
public WebAPIAgent(
IAgentWorker worker,
[FromKeyedServices("EventTypes")] EventTypes typeRegistry,
[FromKeyedServices("AgentsMetadata")] AgentsMetadata typeRegistry,
ILogger<WebAPIAgent> logger,
string url = "/agents/webio") : base(
worker,
typeRegistry)
{
_url = url;
@ -53,7 +52,7 @@ public abstract class WebAPIAgent : IOAgent,
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 abstract class WebAPIAgent : IOAgent,
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

View File

@ -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<T>(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<T>(this CloudEvent cloudEvent) where T : IMessage, new()
{
return cloudEvent.ProtoData.Unpack<T>();
}
public static AgentState ToAgentState<T>(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<T>(this AgentState state) where T : IMessage, new()
{
if (state.HasTextData == true)
{
if (typeof(T) == typeof(AgentState))
{
return (T)(IMessage)state;
}
}
return state.ProtoData.Unpack<T>();
}
}

View File

@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Microsoft.AutoGen.Core;
namespace Microsoft.AutoGen.Core.Grpc;
public sealed class GrpcAgentWorker(
AgentRpc.AgentRpcClient client,
@ -24,6 +24,7 @@ public sealed class GrpcAgentWorker(
private readonly ConcurrentDictionary<string, Type> _agentTypes = new();
private readonly ConcurrentDictionary<(string Type, string Key), Agent> _agents = new();
private readonly ConcurrentDictionary<string, (Agent Agent, string OriginalRequestId)> _pendingRequests = new();
private readonly ConcurrentDictionary<string, HashSet<Type>> _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<Tuple<string, Type>> _configuredAgentTypes = configuredAgentTypes;
private readonly ILogger<GrpcAgentWorker> _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,6 @@ public sealed class GrpcAgentWorker(
message.Response.RequestId = request.OriginalRequestId;
request.Agent.ReceiveMessage(message);
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)
{
var agent = GetOrActivateAgent(new AgentId { Type = typeName, Key = item.Source });
agent.ReceiveMessage(message);
}
break;
case Message.MessageOneofCase.RegisterAgentTypeResponse:
if (!message.RegisterAgentTypeResponse.Success)
{
@ -101,6 +87,24 @@ public sealed class GrpcAgentWorker(
_logger.LogError($"Failed to add subscription '{message.AddSubscriptionResponse.Error}'");
}
break;
case Message.MessageOneofCase.CloudEvent:
var item = message.CloudEvent;
if (!_agentsForEvent.TryGetValue(item.Type, out var agents))
{
_logger.LogError($"This worker can't handle the event type '{item.Type}'.");
break;
}
foreach (var a in agents)
{
var subject = item.GetSubject();
if (string.IsNullOrEmpty(subject))
{
subject = item.Source;
}
var agent = GetOrActivateAgent(new AgentId { Type = a.Name, Key = subject });
agent.ReceiveMessage(message);
}
break;
default:
throw new InvalidOperationException($"Unexpected message '{message}'.");
}
@ -161,10 +165,15 @@ public sealed class GrpcAgentWorker(
_logger.LogError(ex, "Error connecting to GRPC endpoint {Endpoint}.", Environment.GetEnvironmentVariable("AGENT_HOST"));
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;
}
@ -187,7 +196,8 @@ public sealed class GrpcAgentWorker(
{
if (_agentTypes.TryGetValue(agentId.Type, out var agentType))
{
agent = (Agent)ActivatorUtilities.CreateInstance(_serviceProvider, agentType, this);
agent = (Agent)ActivatorUtilities.CreateInstance(ServiceProvider, agentType);
Agent.Initialize(this, agent);
_agents.TryAdd((agentId.Type, agentId.Key), agent);
}
else
@ -206,22 +216,38 @@ public sealed class GrpcAgentWorker(
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<TopicSubscriptionAttribute>().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<Type>();
_agentsForEvent[evt!] = agents;
}
//TODO: do something with the response (like retry on error)
agents.Add(agentType);
}
var topicTypes = agentType.GetCustomAttributes<TopicSubscriptionAttribute>().Select(t => t.Topic).ToList();
/* 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 }, //future
//Events = { events } //future
}
}, cancellationToken).ConfigureAwait(false);
if (!topicTypes.Any())
{
topicTypes.Add(agentType.Name);
}
foreach (var topic in topicTypes)
{
var subscriptionRequest = new Message
@ -239,7 +265,7 @@ public sealed class GrpcAgentWorker(
}
}
};
await WriteChannelAsync(subscriptionRequest, cancellationToken).ConfigureAwait(true);
await _client.AddSubscriptionAsync(subscriptionRequest.AddSubscriptionRequest, null, null, cancellationToken);
foreach (var e in events)
{
subscriptionRequest = new Message
@ -257,7 +283,7 @@ public sealed class GrpcAgentWorker(
}
}
};
await WriteChannelAsync(subscriptionRequest, cancellationToken).ConfigureAwait(true);
await _client.AddSubscriptionAsync(subscriptionRequest.AddSubscriptionRequest, null, null, cancellationToken);
}
}
}
@ -351,8 +377,8 @@ public sealed class GrpcAgentWorker(
try
{
_readTask = Task.Run(RunReadPump, CancellationToken.None);
_writeTask = Task.Run(RunWritePump, CancellationToken.None);
_readTask = Task.Run(RunReadPump, cancellationToken);
_writeTask = Task.Run(RunWritePump, cancellationToken);
}
finally
{
@ -408,5 +434,20 @@ public sealed class GrpcAgentWorker(
throw new KeyNotFoundException($"Failed to read AgentState for {agentId}.");
}
}
public async ValueTask<List<Subscription>> GetSubscriptionsAsync(GetSubscriptionsRequest request, CancellationToken cancellationToken = default)
{
var response = await _client.GetSubscriptionsAsync(request, null, null, cancellationToken);
return response.Subscriptions.ToList();
}
public ValueTask<AddSubscriptionResponse> SubscribeAsync(AddSubscriptionRequest request, CancellationToken cancellationToken = default)
{
var response = _client.AddSubscription(request, null, null, cancellationToken);
return new ValueTask<AddSubscriptionResponse>(response);
}
public ValueTask<RemoveSubscriptionResponse> UnsubscribeAsync(RemoveSubscriptionRequest request, CancellationToken cancellationToken = default)
{
var response = _client.RemoveSubscription(request, null, null, cancellationToken);
return new ValueTask<RemoveSubscriptionResponse>(response);
}
}

View File

@ -1,13 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// GrpcAgentWorkerHostBuilderExtension.cs
using System.Diagnostics;
using System.Reflection;
using Google.Protobuf;
using Google.Protobuf.Reflection;
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;
using Microsoft.Extensions.Logging;
namespace Microsoft.AutoGen.Core.Grpc;
public static class GrpcAgentWorkerHostBuilderExtensions
@ -20,14 +22,27 @@ public static class GrpcAgentWorkerHostBuilderExtensions
options.Address = new Uri(agentServiceAddress ?? builder.Configuration["AGENT_HOST"] ?? _defaultAgentServiceAddress);
options.ChannelOptionsActions.Add(channelOptions =>
{
channelOptions.HttpHandler = new SocketsHttpHandler
var loggerFactory = new LoggerFactory();
if (Debugger.IsAttached)
{
EnableMultipleHttp2Connections = true,
KeepAlivePingDelay = TimeSpan.FromSeconds(20),
KeepAlivePingTimeout = TimeSpan.FromSeconds(10),
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests
};
channelOptions.HttpHandler = new SocketsHttpHandler
{
EnableMultipleHttp2Connections = false,
KeepAlivePingDelay = TimeSpan.FromSeconds(200),
KeepAlivePingTimeout = TimeSpan.FromSeconds(100),
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always
};
}
else
{
channelOptions.HttpHandler = new SocketsHttpHandler
{
EnableMultipleHttp2Connections = true,
KeepAlivePingDelay = TimeSpan.FromSeconds(20),
KeepAlivePingTimeout = TimeSpan.FromSeconds(10),
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests
};
}
var methodConfig = new MethodConfig
{
@ -46,79 +61,19 @@ public static class GrpcAgentWorkerHostBuilderExtensions
channelOptions.ThrowOperationCanceledOnCancellation = true;
});
});
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
builder.Services.TryAddSingleton(DistributedContextPropagator.Current);
builder.Services.AddSingleton<IAgentWorker, GrpcAgentWorker>();
builder.Services.AddKeyedSingleton("EventTypes", (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);
});
builder.Services.AddSingleton<IHostedService>(sp => (IHostedService)sp.GetRequiredService<IAgentWorker>());
builder.Services.AddKeyedSingleton("AgentsMetadata", (sp, key) =>
{
return ReflectionHelper.GetAgentsMetadata(assemblies);
});
builder.Services.AddSingleton((s) =>
{
var worker = s.GetRequiredService<IAgentWorker>();
var client = ActivatorUtilities.CreateInstance<Client>(s);
Agent.Initialize(worker, client);
return client;
});
builder.Services.AddSingleton(new AgentApplicationBuilder(builder));

View File

@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\Core\Microsoft.AutoGen.Core.csproj" />
<ProjectReference Include="..\Contracts\Microsoft.AutoGen.Contracts.csproj" />
<ProjectReference Include="..\Runtime.Grpc\Microsoft.AutoGen.Runtime.Grpc.csproj" />
</ItemGroup>

View File

@ -16,7 +16,7 @@ namespace Microsoft.AutoGen.Core;
/// <summary>
/// Represents the base class for an agent in the AutoGen system.
/// </summary>
public abstract class Agent : IHandle
public abstract class Agent
{
private readonly object _lock = new();
private readonly ConcurrentDictionary<string, TaskCompletionSource<RpcResponse>> _pendingRequests = [];
@ -32,23 +32,25 @@ public abstract class Agent : IHandle
public AgentId AgentId { get; private set; }
private readonly Channel<object> _mailbox = Channel.CreateUnbounded<object>();
protected internal ILogger<Agent> _logger;
public AgentMessenger Messenger { get; private set; }
public IAgentWorker Worker { get; private set; }
private readonly ConcurrentDictionary<Type, MethodInfo> _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<Agent>? logger = null)
{
EventTypes = eventTypes;
AgentId = new AgentId(this.GetType().Name, Guid.NewGuid().ToString()); ;
AgentId = new AgentId(this.GetType().Name, Guid.NewGuid().ToString());
_logger = logger ?? LoggerFactory.Create(builder => { }).CreateLogger<Agent>();
_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()
@ -74,7 +76,7 @@ public abstract class Agent : IHandle
}
};
// 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<T> and subscribe to the topic T
@ -82,13 +84,15 @@ public abstract class Agent : IHandle
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);
}
}
}
}
/// <summary>
@ -148,7 +152,7 @@ public abstract class Agent : IHandle
{
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,66 @@ public abstract class Agent : IHandle
break;
}
}
public List<string> Subscribe(string topic)
public async ValueTask<List<Subscription>> GetSubscriptionsAsync()
{
Message message = new()
GetSubscriptionsRequest request = new();
return await Worker.GetSubscriptionsAsync(request).ConfigureAwait(false);
}
public async ValueTask<AddSubscriptionResponse> SubscribeAsync(string topic)
{
AddSubscriptionRequest 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<string> { topic };
var subscriptionResponse = await Worker.SubscribeAsync(subscriptionRequest).ConfigureAwait(true);
if (!subscriptionResponse.Success)
{
_logger.LogError($"{GetType}{AgentId.Key}: Failed to unsubscribe from topic {topic}");
}
return subscriptionResponse;
}
public async ValueTask<RemoveSubscriptionResponse> UnsubscribeAsync(Guid id)
{
RemoveSubscriptionRequest subscriptionRequest = new()
{
Id = id.ToString()
};
var subscriptionResponse = await Worker.UnsubscribeAsync(subscriptionRequest).ConfigureAwait(true);
if (!subscriptionResponse.Success)
{
_logger.LogError($"{GetType}{AgentId.Key}: Failed to unsubscribe from Subscription {id}");
}
return subscriptionResponse;
}
public async ValueTask<RemoveSubscriptionResponse> UnsubscribeAsync(string topic)
{
var subscriptions = await GetSubscriptionsAsync().ConfigureAwait(false);
var subscription = subscriptions.FirstOrDefault(s => s.TypeSubscription.TopicType == topic);
if (subscription == null)
{
var error = $"{GetType}{AgentId.Key}: Subscription not found for topic {topic}";
_logger.LogError(error);
return new RemoveSubscriptionResponse { Success = false, Error = error };
}
var id = Guid.Parse(subscription.Id);
return await UnsubscribeAsync(id).ConfigureAwait(true);
}
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<T> ReadAsync<T>(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<T>();
}
private void OnResponseCore(RpcResponse response)
@ -226,7 +261,9 @@ public abstract class Agent : IHandle
{
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<RpcResponse> RequestAsync(AgentId target, string method, Dictionary<string, string> parameters)
@ -250,7 +287,7 @@ public abstract class Agent : IHandle
activity?.SetTag("peer.service", target.ToString());
var completion = new TaskCompletionSource<RpcResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
Messenger!.Update(request, activity);
IAgentWorkerExtensions.Update(this.Worker, request, activity);
await this.InvokeWithActivityAsync(
static async (state, ct) =>
{
@ -258,7 +295,7 @@ public abstract class Agent : IHandle
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);
},
@ -270,101 +307,155 @@ public abstract class Agent : IHandle
return await completion.Task.ConfigureAwait(false);
}
public async ValueTask PublishMessageAsync<T>(T message, string? source = null, CancellationToken token = default) where T : IMessage
private string SetTopic(string? topic = null, string? source = null, string? key = null)
{
var topicTypes = this.GetType().GetCustomAttributes<TopicSubscriptionAttribute>().Select(t => t.Topic);
if (!topicTypes.Any())
if (string.IsNullOrWhiteSpace(topic))
{
topicTypes = topicTypes.Append(string.IsNullOrWhiteSpace(source) ? this.AgentId.Type + "." + this.AgentId.Key : source);
topic = this.AgentId.Type + "." + this.AgentId.Key;
}
foreach (var topic in topicTypes)
else
{
await PublishMessageAsync(topic, message, source, token).ConfigureAwait(false);
topic = topic + "." + source + "." + key;
}
}
public async ValueTask PublishMessageAsync<T>(string topic, T message, string? source = null, CancellationToken token = default) where T : IMessage
{
await PublishEventAsync(topic, message, token).ConfigureAwait(false);
return topic;
}
/// <summary>
/// Publishes a message asynchronously.
/// </summary>
/// <typeparam name="T">The type of the message.</typeparam>
/// <param name="token">A token to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async ValueTask PublishMessageAsync<T>(T message, string topic, string source, string key, CancellationToken token = default) where T : IMessage
{
// if there are no topic types, use the agent's default topic subscription attribute and the agent's type and key
if (string.IsNullOrWhiteSpace(topic))
{
if (string.IsNullOrWhiteSpace(topic))
{
topic = this.AgentId.Type + "." + this.AgentId.Key;
}
else
{
topic = topic + "." + source + "." + key;
}
var topicTypes = this.GetType().GetCustomAttributes<TopicSubscriptionAttribute>().Select(t => t.Topic);
if (!topicTypes.Any())
{
topicTypes = topicTypes.Append(string.IsNullOrWhiteSpace(source) ? this.AgentId.Type + "." + this.AgentId.Key : source);
}
topicTypes = topicTypes.Append(SetTopic(topic, source, key));
foreach (var t in topicTypes)
{
await PublishEventAsync(t, message, token).ConfigureAwait(false);
}
}
else
{
await PublishEventAsync(topic, message, token).ConfigureAwait(false);
}
}
public async ValueTask PublishMessageAsync<T>(T message, string topic, string source, CancellationToken token = default) where T : IMessage
{
string key = this.AgentId.Key;
await PublishMessageAsync(message, topic, source, key, token).ConfigureAwait(false);
}
public async ValueTask PublishMessageAsync<T>(T message, string topic, CancellationToken token = default) where T : IMessage
{
string source = this.AgentId.Type;
string key = this.AgentId.Key;
await PublishMessageAsync(message, topic, source, key, token).ConfigureAwait(false);
}
public async ValueTask PublishMessageAsync<T>(T message, CancellationToken token = default) where T : IMessage
{
string topic = "";
string source = this.AgentId.Type;
string key = this.AgentId.Key;
await PublishMessageAsync(message, topic, source, key, token).ConfigureAwait(false);
}
public async ValueTask PublishEventAsync(string topic, IMessage message, CancellationToken cancellationToken = default)
{
await PublishEventAsync(message.ToCloudEvent(key: GetType().Name, topic: topic), cancellationToken).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<object>.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<IMessage>.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<object>.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<IMessage>.Handle)}");
throw; // TODO: ?
}
}
return Task.CompletedTask;
}
public Task<RpcResponse> HandleRequestAsync(RpcRequest request) => Task.FromResult(new RpcResponse { Error = "Not implemented" });
//TODO: should this be async and cancellable?
public virtual Task HandleObject(object item)
/// <summary>
/// Handles a generic object
/// </summary>
/// <param name="item">The object to handle</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// TODO: this is only called from tests, should we remove it?
public virtual async Task HandleObjectAsync(object item, CancellationToken cancellationToken = default)
{
// get all Handle<T> 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}");
}
public async ValueTask PublishEventAsync(string topic, IMessage message, CancellationToken cancellationToken = default)
{
await PublishEventAsync(message.ToCloudEvent(topic), cancellationToken).ConfigureAwait(false);
_logger.LogError($"No handler found for type {item.GetType().FullName}");
}
}

View File

@ -22,7 +22,7 @@ public static class AgentExtensions
public static Activity? ExtractActivity(this Agent agent, string activityName, IDictionary<string, string> 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)
{

View File

@ -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<string, string> metadata)
{
DistributedContextPropagator.ExtractTraceIdAndState(metadata,
static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
{
var metadata = (IDictionary<string, string>)carrier!;
fieldValues = null;
metadata.TryGetValue(fieldName, out fieldValue);
},
out var traceParent,
out var traceState);
return (traceParent, traceState);
}
public (string?, string?) GetTraceIdAndState(MapField<string, CloudEventAttributeValue> metadata)
{
DistributedContextPropagator.ExtractTraceIdAndState(metadata,
static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
{
var metadata = (MapField<string, CloudEventAttributeValue>)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<string, string>)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<string, CloudEventAttributeValue>)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<AgentState> ReadAsync(AgentId agentId, CancellationToken cancellationToken = default)
{
return worker.ReadAsync(agentId, cancellationToken);
}
public IDictionary<string, string> ExtractMetadata(IDictionary<string, string> metadata)
{
var baggage = DistributedContextPropagator.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
{
var metadata = (IDictionary<string, string>)carrier!;
fieldValues = null;
metadata.TryGetValue(fieldName, out fieldValue);
});
return baggage as IDictionary<string, string> ?? new Dictionary<string, string>();
}
public IDictionary<string, string> ExtractMetadata(MapField<string, CloudEventAttributeValue> metadata)
{
var baggage = DistributedContextPropagator.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
{
var metadata = (MapField<string, CloudEventAttributeValue>)carrier!;
fieldValues = null;
metadata.TryGetValue(fieldName, out var ceValue);
fieldValue = ceValue?.CeString;
});
return baggage as IDictionary<string, string> ?? new Dictionary<string, string>();
}
}

View File

@ -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);
}
}

View File

@ -9,12 +9,19 @@ using Microsoft.Extensions.Hosting;
namespace Microsoft.AutoGen.Core;
/// <summary>
/// Represents a worker that manages agents and handles messages.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="AgentWorker"/> class.
/// </remarks>
/// <param name="hostApplicationLifetime">The application lifetime.</param>
/// <param name="serviceProvider">The service provider.</param>
/// <param name="configuredAgentTypes">The configured agent types.</param>
public class AgentWorker(
IHostApplicationLifetime hostApplicationLifetime,
IServiceProvider serviceProvider,
[FromKeyedServices("AgentTypes")] IEnumerable<Tuple<string, Type>> configuredAgentTypes) :
IHostedService,
IAgentWorker
IHostApplicationLifetime hostApplicationLifetime,
IServiceProvider serviceProvider,
[FromKeyedServices("AgentTypes")] IEnumerable<Tuple<string, Type>> configuredAgentTypes) : IHostedService, IAgentWorker
{
private readonly ConcurrentDictionary<string, Type> _agentTypes = new();
private readonly ConcurrentDictionary<(string Type, string Key), Agent> _agents = new();
@ -22,24 +29,27 @@ IServiceProvider serviceProvider,
private readonly ConcurrentDictionary<string, AgentState> _agentStates = new();
private readonly ConcurrentDictionary<string, (Agent Agent, string OriginalRequestId)> _pendingClientRequests = new();
private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping);
private readonly IServiceProvider _serviceProvider = serviceProvider;
public IServiceProvider ServiceProvider { get; } = serviceProvider;
private readonly IEnumerable<Tuple<string, Type>> _configuredAgentTypes = configuredAgentTypes;
private readonly ConcurrentDictionary<string, Subscription> _subscriptionsByAgentType = new();
private readonly ConcurrentDictionary<string, List<Subscription>> _subscriptionsByAgentType = new();
private readonly ConcurrentDictionary<string, List<string>> _subscriptionsByTopic = new();
private readonly ConcurrentDictionary<Guid, IDictionary<string, string>> _subscriptionsByGuid = 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
/// <inheritdoc />
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 });
}
}
/// <inheritdoc />
public async ValueTask SendRequestAsync(Agent agent, RpcRequest request, CancellationToken cancellationToken = default)
{
var requestId = Guid.NewGuid().ToString();
@ -47,21 +57,28 @@ IServiceProvider serviceProvider,
request.RequestId = requestId;
await _mailbox.Writer.WriteAsync(request, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public ValueTask SendResponseAsync(RpcResponse response, CancellationToken cancellationToken = default)
{
return _mailbox.Writer.WriteAsync(new Message { Response = response }, cancellationToken);
}
/// <inheritdoc />
public ValueTask SendMessageAsync(Message message, CancellationToken cancellationToken = default)
{
return _mailbox.Writer.WriteAsync(message, cancellationToken);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public ValueTask<AgentState> ReadAsync(AgentId agentId, CancellationToken cancellationToken = default)
{
_agentStates.TryGetValue(agentId.ToString(), out var state);
@ -74,6 +91,10 @@ IServiceProvider serviceProvider,
throw new KeyNotFoundException($"Failed to read AgentState for {agentId}.");
}
}
/// <summary>
/// Runs the message pump.
/// </summary>
public async Task RunMessagePump()
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
@ -95,7 +116,7 @@ IServiceProvider serviceProvider,
}
break;
case Message msg when msg.AddSubscriptionRequest != null:
await AddSubscriptionRequestAsync(msg.AddSubscriptionRequest).ConfigureAwait(true);
await SubscribeAsync(msg.AddSubscriptionRequest).ConfigureAwait(true);
break;
case Message msg when msg.AddSubscriptionResponse != null:
break;
@ -114,24 +135,70 @@ IServiceProvider serviceProvider,
}
}
}
private async ValueTask AddSubscriptionRequestAsync(AddSubscriptionRequest subscription)
public async ValueTask<AddSubscriptionResponse> SubscribeAsync(AddSubscriptionRequest subscription, CancellationToken cancellationToken = default)
{
var topic = subscription.Subscription.TypeSubscription.TopicType;
var agentType = subscription.Subscription.TypeSubscription.AgentType;
_subscriptionsByAgentType[agentType] = subscription.Subscription;
var id = Guid.NewGuid();
subscription.Subscription.Id = id.ToString();
var sub = new Dictionary<string, string> { { topic, agentType } };
_subscriptionsByGuid.GetOrAdd(id, static _ => new Dictionary<string, string>()).Add(topic, agentType);
_subscriptionsByAgentType.GetOrAdd(key: agentType, _ => []).Add(subscription.Subscription);
_subscriptionsByTopic.GetOrAdd(topic, _ => []).Add(agentType);
Message response = new()
var response = new AddSubscriptionResponse
{
AddSubscriptionResponse = new()
{
RequestId = subscription.RequestId,
Error = "",
Success = true
}
RequestId = subscription.RequestId,
Error = "",
Success = true
};
await _mailbox.Writer.WriteAsync(response).ConfigureAwait(false);
return response;
}
public async ValueTask<RemoveSubscriptionResponse> UnsubscribeAsync(RemoveSubscriptionRequest request, CancellationToken cancellationToken = default)
{
if (!Guid.TryParse(request.Id, out var id))
{
var removeSubscriptionResponse = new RemoveSubscriptionResponse
{
Error = "Invalid subscription ID",
Success = false
};
return removeSubscriptionResponse;
}
if (_subscriptionsByGuid.TryGetValue(id, out var sub))
{
foreach (var (topic, agentType) in sub)
{
if (_subscriptionsByTopic.TryGetValue(topic, out var innerAgentTypes))
{
while (innerAgentTypes.Remove(agentType))
{
//ensures all instances are removed
}
_subscriptionsByTopic.AddOrUpdate(topic, innerAgentTypes, (_, _) => innerAgentTypes);
}
var toRemove = new List<Subscription>();
if (_subscriptionsByAgentType.TryGetValue(agentType, out var innerSubscriptions))
{
foreach (var subscription in innerSubscriptions)
{
if (subscription.Id == id.ToString())
{
toRemove.Add(subscription);
}
}
foreach (var subscription in toRemove) { innerSubscriptions.Remove(subscription); }
_subscriptionsByAgentType.AddOrUpdate(agentType, innerSubscriptions, (_, _) => innerSubscriptions);
}
}
_subscriptionsByGuid.TryRemove(id, out _);
}
var response = new RemoveSubscriptionResponse
{
Error = "",
Success = true
};
return response;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
StartCore();
@ -162,6 +229,8 @@ IServiceProvider serviceProvider,
}
}
}
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
_shutdownCts.Cancel();
@ -176,14 +245,26 @@ IServiceProvider serviceProvider,
{
}
}
/// <summary>
/// Gets or activates an agent.
/// </summary>
/// <param name="agentId">The agent ID.</param>
/// <returns>The activated agent.</returns>
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 +274,21 @@ IServiceProvider serviceProvider,
return agent;
}
public ValueTask<List<Subscription>> GetSubscriptionsAsync(Type type)
{
if (_subscriptionsByAgentType.TryGetValue(type.Name, out var subscriptions))
{
return new ValueTask<List<Subscription>>(subscriptions);
}
return new ValueTask<List<Subscription>>([]);
}
public ValueTask<List<Subscription>> GetSubscriptionsAsync(GetSubscriptionsRequest request, CancellationToken cancellationToken = default)
{
var subscriptions = new List<Subscription>();
foreach (var (_, value) in _subscriptionsByAgentType)
{
subscriptions.AddRange(value);
}
return new ValueTask<List<Subscription>>(subscriptions);
}
}

View File

@ -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;
/// <summary>
/// Represents a collection of event types and their associated metadata.
/// </summary>
public sealed class AgentsMetadata
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentsMetadata"/> class.
/// </summary>
/// <param name="typeRegistry">The type registry containing protobuf type information.</param>
/// <param name="types">A dictionary mapping event names to their corresponding types.</param>
/// <param name="eventsMap">A dictionary mapping types to a set of event names associated with those types.</param>
public AgentsMetadata(TypeRegistry typeRegistry, Dictionary<string, Type> types, Dictionary<Type, HashSet<string>> eventsMap, Dictionary<Type, HashSet<string>> topicsMap)
{
TypeRegistry = typeRegistry;
_types = new(types);
_eventsMap = new(eventsMap);
_topicsMap = new(topicsMap);
}
/// <summary>
/// Gets the type registry containing protobuf type information.
/// </summary>
public TypeRegistry TypeRegistry { get; }
private ConcurrentDictionary<string, Type> _types;
private ConcurrentDictionary<Type, HashSet<string>> _eventsMap;
private ConcurrentDictionary<Type, HashSet<string>> _topicsMap;
/// <summary>
/// Checks if a given type handles a specific event name.
/// </summary>
/// <param name="type">The type to check.</param>
/// <param name="eventName">The event name to check.</param>
/// <returns><c>true</c> if the type handles the event name; otherwise, <c>false</c>.</returns>
public bool CheckIfTypeHandles(Type type, string eventName)
{
if (_eventsMap.TryGetValue(type, out var events))
{
return events.Contains(eventName);
}
return false;
}
/// <summary>
/// Gets the event type by its name.
/// </summary>
/// <param name="type">The name of the event type.</param>
/// <returns>The event type if found; otherwise, <c>null</c>.</returns>
public Type? GetEventTypeByName(string type)
{
if (_types.TryGetValue(type, out var eventType))
{
return eventType;
}
return null;
}
public HashSet<string>? GetEventsForAgent(Type agent)
{
if (_eventsMap.TryGetValue(agent, out var events))
{
return events;
}
return null;
}
public HashSet<string>? GetTopicsForAgent(Type agent)
{
if (_topicsMap.TryGetValue(agent, out var topics))
{
return topics;
}
return null;
}
}

View File

@ -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)
{
}

View File

@ -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 class HostBuilderExtensions
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<IAgentWorker, AgentWorker>();
builder.Services.AddSingleton<IHostedService>(sp => (IHostedService)sp.GetRequiredService<IAgentWorker>());
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<IAgentWorker>();
var client = ActivatorUtilities.CreateInstance<Client>(s);
Agent.Initialize(worker, client);
return client;
});
builder.Services.AddSingleton<Client>();
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)
{

View File

@ -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<AgentState> ReadAsync(AgentId agentId, CancellationToken cancellationToken = default);
ValueTask<AddSubscriptionResponse> SubscribeAsync(AddSubscriptionRequest request, CancellationToken cancellationToken = default);
ValueTask<RemoveSubscriptionResponse> UnsubscribeAsync(RemoveSubscriptionRequest request, CancellationToken cancellationToken = default);
ValueTask<List<Subscription>> GetSubscriptionsAsync(GetSubscriptionsRequest request, CancellationToken cancellationToken = default);
}

View File

@ -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<string, string> metadata)
{
var dcp = worker.ServiceProvider.GetRequiredService<DistributedContextPropagator>();
dcp.ExtractTraceIdAndState(metadata,
static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
{
var metadata = (IDictionary<string, string>)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<string, CloudEventAttributeValue> metadata)
{
var dcp = worker.ServiceProvider.GetRequiredService<DistributedContextPropagator>();
dcp.ExtractTraceIdAndState(metadata,
static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
{
var metadata = (MapField<string, CloudEventAttributeValue>)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<DistributedContextPropagator>();
dcp.Inject(activity, request.Metadata, static (carrier, key, value) =>
{
var metadata = (IDictionary<string, string>)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<DistributedContextPropagator>();
dcp.Inject(activity, cloudEvent.Attributes, static (carrier, key, value) =>
{
var mapField = (MapField<string, CloudEventAttributeValue>)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<string, string> ExtractMetadata(IAgentWorker worker, IDictionary<string, string> metadata)
{
var dcp = worker.ServiceProvider.GetRequiredService<DistributedContextPropagator>();
var baggage = dcp.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
{
var metadata = (IDictionary<string, string>)carrier!;
fieldValues = null;
metadata.TryGetValue(fieldName, out fieldValue);
});
return baggage as IDictionary<string, string> ?? new Dictionary<string, string>();
}
public static IDictionary<string, string> ExtractMetadata(IAgentWorker worker, MapField<string, CloudEventAttributeValue> metadata)
{
var dcp = worker.ServiceProvider.GetRequiredService<DistributedContextPropagator>();
var baggage = dcp.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
{
var metadata = (MapField<string, CloudEventAttributeValue>)carrier!;
fieldValues = null;
metadata.TryGetValue(fieldName, out var ceValue);
fieldValue = ceValue?.CeString;
});
return baggage as IDictionary<string, string> ?? new Dictionary<string, string>();
}
}

View File

@ -1,12 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// IHandle.cs
namespace Microsoft.AutoGen.Core;
public interface IHandle
{
Task HandleObject(object item);
}
public interface IHandle<T> : IHandle
using Google.Protobuf;
namespace Microsoft.AutoGen.Core;
/// <summary>
/// Defines a handler interface for processing items of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of item to be handled, which must implement <see cref="IMessage"/>.</typeparam>
public interface IHandle<in T> where T : IMessage
{
Task Handle(T item);
/// <summary>
/// Handles the specified item asynchronously.
/// </summary>
/// <param name="item">The item to be handled.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task Handle(T item, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// MessageExtensions.cs
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AutoGen.Contracts;
namespace Microsoft.AutoGen.Core;
/// <summary>
/// Provides extension methods for converting messages to and from various formats.
/// </summary>
public static class MessageExtensions
{
private const string PROTO_DATA_CONTENT_TYPE = "application/x-protobuf";
/// <summary>
/// Converts a message to a CloudEvent.
/// </summary>
/// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message to convert.</param>
/// <param name="key">The key of the event, maps to the Topic Type</param>
/// <param name="topic">The topic of the event, </param>
/// <returns>A CloudEvent representing the message.</returns>
public static CloudEvent ToCloudEvent<T>(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 }
}
}
};
}
/// <summary>
/// Converts a CloudEvent back to a message.
/// </summary>
/// <typeparam name="T">The type of the message.</typeparam>
/// <param name="cloudEvent">The CloudEvent to convert.</param>
/// <returns>The message represented by the CloudEvent.</returns>
public static T FromCloudEvent<T>(this CloudEvent cloudEvent) where T : IMessage, new()
{
return cloudEvent.ProtoData.Unpack<T>();
}
/// <summary>
public static string GetSubject(this CloudEvent cloudEvent)
{
if (cloudEvent.Attributes.TryGetValue("subject", out var value))
{
return value.CeString;
}
else
{
return string.Empty;
}
}
/// <summary>
/// Converts a state to an AgentState.
/// </summary>
/// <typeparam name="T">The type of the state.</typeparam>
/// <param name="state">The state to convert.</param>
/// <param name="agentId">The ID of the agent.</param>
/// <param name="eTag">The ETag of the state.</param>
/// <returns>An AgentState representing the state.</returns>
public static AgentState ToAgentState<T>(this T state, AgentId agentId, string eTag) where T : IMessage
{
return new AgentState
{
ProtoData = Any.Pack(state),
AgentId = agentId,
ETag = eTag
};
}
/// <summary>
/// Converts an AgentState back to a state.
/// </summary>
/// <typeparam name="T">The type of the state.</typeparam>
/// <param name="state">The AgentState to convert.</param>
/// <returns>The state represented by the AgentState.</returns>
public static T FromAgentState<T>(this AgentState state) where T : IMessage, new()
{
if (state.HasTextData == true)
{
if (typeof(T) == typeof(AgentState))
{
return (T)(IMessage)state;
}
}
return state.ProtoData.Unpack<T>();
}
}

View File

@ -23,7 +23,7 @@ public sealed class ReflectionHelper
}
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 sealed class ReflectionHelper
.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<TopicSubscriptionAttribute>().Select(a => a.Topic).ToHashSet()))
.ToDictionary(item => item.t, item => item.Item2);
return new AgentsMetadata(typeRegistry, types, eventsMap, topicsMap);
}
/// <summary>

View File

@ -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

View File

@ -0,0 +1,24 @@
// 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<AgentState> 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<List<Subscription>> GetSubscriptionsAsync(Type type) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage);
public ValueTask<List<Subscription>> GetSubscriptionsAsync(GetSubscriptionsRequest request, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage);
public ValueTask<AddSubscriptionResponse> SubscribeAsync(AddSubscriptionRequest request, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage);
public ValueTask<RemoveSubscriptionResponse> UnsubscribeAsync(RemoveSubscriptionRequest request, CancellationToken cancellationToken = default) => throw new AgentInitalizedIncorrectlyException(AgentNotInitializedMessage);
public class AgentInitalizedIncorrectlyException(string message) : Exception(message)
{
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// IAgentGrain.cs
namespace Microsoft.AutoGen.Runtime.Grpc.Abstractions;
internal interface IAgentGrain : IGrainWithStringKey
{
ValueTask<Contracts.AgentState> ReadStateAsync();
ValueTask<string> WriteStateAsync(Contracts.AgentState state, string eTag);
}

View File

@ -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<RpcResponse> InvokeRequestAsync(RpcRequest request);
ValueTask BroadcastEventAsync(CloudEvent evt);
ValueTask StoreAsync(Contracts.AgentState value);
ValueTask<Contracts.AgentState> ReadAsync(AgentId agentId);
ValueTask<RegisterAgentTypeResponse> RegisterAgentTypeAsync(RegisterAgentTypeRequest request);
ValueTask<AddSubscriptionResponse> SubscribeAsync(AddSubscriptionRequest request);
ValueTask<RemoveSubscriptionResponse> UnsubscribeAsync(RemoveSubscriptionRequest request);
ValueTask<List<Subscription>> GetSubscriptionsAsync(GetSubscriptionsRequest request);
Task SendMessageAsync(IConnection connection, CloudEvent cloudEvent);
}

View File

@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// IRegistry.cs
using Microsoft.AutoGen.Contracts;
namespace Microsoft.AutoGen.Runtime.Grpc.Abstractions;
/// <summary>
/// Interface for managing agent registration, placement, and subscriptions.
/// </summary>
public interface IRegistry
{
/// <summary>
/// Gets or places an agent based on the provided agent ID.
/// </summary>
/// <param name="agentId">The ID of the agent.</param>
/// <returns>A tuple containing the worker and a boolean indicating if it's a new placement.</returns>
ValueTask<(IGateway? Worker, bool NewPlacement)> GetOrPlaceAgent(AgentId agentId);
/// <summary>
/// Removes a worker from the registry.
/// </summary>
/// <param name="worker">The worker to remove.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask RemoveWorker(IGateway worker);
/// <summary>
/// Registers a new agent type with the specified worker.
/// </summary>
/// <param name="request">The request containing agent type details.</param>
/// <param name="worker">The worker to register the agent type with.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask RegisterAgentType(RegisterAgentTypeRequest request, IGateway worker);
/// <summary>
/// Adds a new worker to the registry.
/// </summary>
/// <param name="worker">The worker to add.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask AddWorker(IGateway worker);
/// <summary>
/// Unregisters an agent type from the specified worker.
/// </summary>
/// <param name="type">The type of the agent to unregister.</param>
/// <param name="worker">The worker to unregister the agent type from.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask UnregisterAgentType(string type, IGateway worker);
/// <summary>
/// Gets a compatible worker for the specified agent type.
/// </summary>
/// <param name="type">The type of the agent.</param>
/// <returns>A task representing the asynchronous operation, with the compatible worker as the result.</returns>
ValueTask<IGateway?> GetCompatibleWorker(string type);
/// <summary>
/// Gets a list of agents subscribed to and handling the specified topic and event type.
/// </summary>
/// <param name="topic">The topic to check subscriptions for.</param>
/// <param name="eventType">The event type to check subscriptions for.</param>
/// <returns>A task representing the asynchronous operation, with the list of agent IDs as the result.</returns>
ValueTask<List<string>> GetSubscribedAndHandlingAgents(string topic, string eventType);
/// <summary>
/// Subscribes an agent to a topic.
/// </summary>
/// <param name="request">The subscription request.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask SubscribeAsync(AddSubscriptionRequest request);
/// <summary>
/// Unsubscribes an agent from a topic.
/// </summary>
/// <param name="request">The unsubscription request.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask UnsubscribeAsync(RemoveSubscriptionRequest request); // TODO: This should have its own request type.
/// <summary>
/// Gets the subscriptions for a specified agent type.
/// </summary>
/// <returns>A task representing the asynchronous operation, with the subscriptions as the result.</returns>
ValueTask<List<Subscription>> GetSubscriptions(GetSubscriptionsRequest request);
}

View File

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// IRegistryGrain.cs
namespace Microsoft.AutoGen.Runtime.Grpc.Abstractions;
/// <summary>
/// Orleans specific interface, needed to mark the key
/// </summary>
[Alias("Microsoft.AutoGen.Runtime.Grpc.Abstractions.IRegistryGrain")]
public interface IRegistryGrain : IRegistry, IGrainWithIntegerKey
{ }

View File

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Contracts\Microsoft.AutoGen.Contracts.csproj" />
<ProjectReference Include="..\Core\Microsoft.AutoGen.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Reminders" />

View File

@ -3,6 +3,7 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AutoGen.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
@ -17,6 +18,10 @@ public static class AgentWorkerHostingExtensions
builder.Services.TryAddSingleton(DistributedContextPropagator.Current);
builder.Services.AddGrpc();
builder.Services.AddKeyedSingleton("AgentsMetadata", (sp, key) =>
{
return ReflectionHelper.GetAgentsMetadata(AppDomain.CurrentDomain.GetAssemblies());
});
builder.Services.AddSingleton<GrpcGateway>();
builder.Services.AddSingleton<IHostedService>(sp => (IHostedService)sp.GetRequiredService<GrpcGateway>());

View File

@ -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<GrpcGateway> _logger;
private readonly IClusterClient _clusterClient;
private readonly ConcurrentDictionary<string, AgentState> _agentState = new();
//private readonly ConcurrentDictionary<string, AgentState> _agentState = new();
private readonly IRegistryGrain _gatewayRegistry;
private readonly ISubscriptionsGrain _subscriptions;
private readonly IGateway _reference;
// The agents supported by each worker process.
private readonly ConcurrentDictionary<string, List<GrpcWorkerConnection>> _supportedAgentTypes = [];
public readonly ConcurrentDictionary<IConnection, IConnection> _workers = new();
internal readonly ConcurrentDictionary<string, GrpcWorkerConnection> _workersByConnection = new();
private readonly ConcurrentDictionary<string, Subscription> _subscriptionsByAgentType = new();
private readonly ConcurrentDictionary<string, List<string>> _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<RpcResponse>> _pendingRequests = new();
// InMemory Message Queue
public GrpcGateway(IClusterClient clusterClient, ILogger<GrpcGateway> logger)
{
_logger = logger;
@ -38,31 +38,89 @@ public sealed class GrpcGateway : BackgroundService, IGateway
_gatewayRegistry = clusterClient.GetGrain<IRegistryGrain>(0);
_subscriptions = clusterClient.GetGrain<ISubscriptionsGrain>(0);
}
public async ValueTask BroadcastEvent(CloudEvent evt)
public async ValueTask<RpcResponse> InvokeRequestAsync(RpcRequest request, CancellationToken cancellationToken = default)
{
var tasks = new List<Task>(_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)
{
// 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." });
}
}
// 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;
}
public async ValueTask StoreAsync(AgentState value, CancellationToken cancellationToken = default)
{
_ = value.AgentId ?? throw new ArgumentNullException(nameof(value.AgentId));
var agentState = _clusterClient.GetGrain<IAgentGrain>($"{value.AgentId.Type}:{value.AgentId.Key}");
await agentState.WriteStateAsync(value, value.ETag);
}
public async ValueTask<AgentState> ReadAsync(AgentId agentId, CancellationToken cancellationToken = default)
{
var agentState = _clusterClient.GetGrain<IAgentGrain>($"{agentId.Type}:{agentId.Key}");
return await agentState.ReadStateAsync();
}
public async ValueTask<RegisterAgentTypeResponse> RegisterAgentTypeAsync(RegisterAgentTypeRequest request, CancellationToken cancellationToken = default)
{
try
{
var connection = _workersByConnection[request.RequestId];
connection.AddSupportedType(request.Type);
_supportedAgentTypes.GetOrAdd(request.Type, _ => []).Add(connection);
tasks.Add(this.SendMessageAsync((IConnection)connection[0], evt, default));
await _gatewayRegistry.RegisterAgentType(request, _reference).ConfigureAwait(true);
return new RegisterAgentTypeResponse
{
Success = true,
RequestId = request.RequestId
};
}
await Task.WhenAll(tasks).ConfigureAwait(false);
}
//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)
{
var queue = (GrpcWorkerConnection)connection;
await queue.ResponseStream.WriteAsync(new Message { CloudEvent = cloudEvent }, cancellationToken).ConfigureAwait(false);
}
private void DispatchResponse(GrpcWorkerConnection connection, RpcResponse response)
{
if (!_pendingRequests.TryRemove((connection, response.RequestId), out var completion))
catch (Exception ex)
{
_logger.LogWarning("Received response for unknown request.");
return;
return new RegisterAgentTypeResponse
{
Success = false,
RequestId = request.RequestId,
Error = ex.Message
};
}
}
public async ValueTask<AddSubscriptionResponse> SubscribeAsync(AddSubscriptionRequest request, CancellationToken cancellationToken = default)
{
try
{
await _gatewayRegistry.SubscribeAsync(request).ConfigureAwait(true);
return new AddSubscriptionResponse
{
Success = true,
RequestId = request.RequestId
};
}
catch (Exception ex)
{
return new AddSubscriptionResponse
{
Success = false,
RequestId = request.RequestId,
Error = ex.Message
};
}
// Complete the request.
completion.SetResult(response);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
@ -87,8 +145,19 @@ public sealed class GrpcGateway : BackgroundService, IGateway
_logger.LogWarning(exception, "Error removing worker from registry.");
}
}
//new is intentional...
internal async Task OnReceivedMessageAsync(GrpcWorkerConnection connection, Message message)
internal async Task ConnectToWorkerProcess(IAsyncStreamReader<Message> requestStream, IServerStreamWriter<Message> responseStream, ServerCallContext context)
{
_logger.LogInformation("Received new connection from {Peer}.", context.Peer);
var workerProcess = new GrpcWorkerConnection(this, requestStream, responseStream, context);
_workers.GetOrAdd(workerProcess, workerProcess);
_workersByConnection.GetOrAdd(context.Peer, workerProcess);
await workerProcess.Connect().ConfigureAwait(false);
}
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, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Received message {Message} from connection {Connection}.", message, connection);
switch (message.MessageCase)
@ -100,7 +169,7 @@ public sealed class GrpcGateway : BackgroundService, IGateway
DispatchResponse(connection, message.Response);
break;
case Message.MessageOneofCase.CloudEvent:
await DispatchEventAsync(message.CloudEvent);
await DispatchEventAsync(message.CloudEvent, cancellationToken);
break;
case Message.MessageOneofCase.RegisterAgentTypeRequest:
await RegisterAgentTypeAsync(connection, message.RegisterAgentTypeRequest);
@ -114,48 +183,22 @@ public sealed class GrpcGateway : BackgroundService, IGateway
break;
};
}
private async ValueTask RespondBadRequestAsync(GrpcWorkerConnection connection, string error)
private void DispatchResponse(GrpcWorkerConnection connection, RpcResponse response)
{
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)
{
var topic = "";
var agentType = "";
if (request.Subscription.TypePrefixSubscription is not null)
if (!_pendingRequests.TryRemove((connection, response.RequestId), out var completion))
{
topic = request.Subscription.TypePrefixSubscription.TopicTypePrefix;
agentType = request.Subscription.TypePrefixSubscription.AgentType;
_logger.LogWarning("Received response for unknown request id: {RequestId}.", response.RequestId);
return;
}
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 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()
@ -167,55 +210,34 @@ public sealed class GrpcGateway : BackgroundService, IGateway
};
await connection.ResponseStream.WriteAsync(response).ConfigureAwait(false);
}
private async ValueTask DispatchEventAsync(CloudEvent evt)
private async ValueTask DispatchEventAsync(CloudEvent evt, CancellationToken cancellationToken = default)
{
// get the event type and then send to all agents that are subscribed to that event type
var eventType = evt.Type;
var source = evt.Source;
var agentTypes = new List<string>();
// 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 agentTypesList)) { agentTypes.AddRange(agentTypesList); }
if (_subscriptionsByTopic.TryGetValue(source, out var agentTypesList2)) { agentTypes.AddRange(agentTypesList2); }
if (_subscriptionsByTopic.TryGetValue(source + "." + eventType, out var agentTypesList3)) { agentTypes.AddRange(agentTypesList3); }
agentTypes = agentTypes.Distinct().ToList();
if (agentTypes.Count > 0)
var registry = _clusterClient.GetGrain<IRegistryGrain>(0);
//intentionally blocking
var targetAgentTypes = await registry.GetSubscribedAndHandlingAgents(evt.Source, evt.Type).ConfigureAwait(true);
if (targetAgentTypes is not null && targetAgentTypes.Count > 0)
{
await DispatchEventToAgentsAsync(agentTypes, evt);
}
// 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)))
{
_subscriptionsByTopic.Where(
kvp => kvp.Key.StartsWith(eventType))
.SelectMany(kvp => kvp.Value)
.Distinct()
.ToList()
.ForEach(async agentType =>
targetAgentTypes = targetAgentTypes.Distinct().ToList();
var tasks = new List<Task>(targetAgentTypes.Count);
foreach (var agentType in targetAgentTypes)
{
if (_supportedAgentTypes.TryGetValue(agentType, out var connections))
{
await DispatchEventToAgentsAsync(new List<string> { agentType }, evt).ConfigureAwait(false);
});
// if the connection is alive, add it to the set, if not remove the connection from the list
var activeConnections = connections.Where(c => c.Completion?.IsCompleted == false).ToList();
foreach (var connection in activeConnections)
{
tasks.Add(this.SendMessageAsync(connection, evt, cancellationToken));
}
}
}
}
else
{
// log that no agent types were found
_logger.LogWarning("No agent types found for event type {EventType}.", eventType);
_logger.LogWarning("No agent types found for event type {EventType}.", evt.Type);
}
}
private async ValueTask DispatchEventToAgentsAsync(IEnumerable<string> agentTypes, CloudEvent evt)
{
var tasks = new List<Task>(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;
@ -235,11 +257,9 @@ public sealed class GrpcGateway : BackgroundService, IGateway
// 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<RpcRequest, Task<RpcResponse>> func)
{
try
@ -253,27 +273,6 @@ public sealed class GrpcGateway : BackgroundService, IGateway
await connection.ResponseStream.WriteAsync(new Message { Response = new RpcResponse { RequestId = request.RequestId, Error = ex.Message } }).ConfigureAwait(false);
}
}
internal Task ConnectToWorkerProcess(IAsyncStreamReader<Message> requestStream, IServerStreamWriter<Message> 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<AgentState> 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 _);
@ -294,41 +293,128 @@ public sealed class GrpcGateway : BackgroundService, IGateway
}
}
}
public async ValueTask<RpcResponse> InvokeRequest(RpcRequest request, CancellationToken cancellationToken = default)
private static async ValueTask RespondBadRequestAsync(GrpcWorkerConnection connection, string error)
{
(string Type, string Key) agentId = (request.Target.Type, request.Target.Key);
if (!_agentDirectory.TryGetValue(agentId, out var connection) || connection.Completion.IsCompleted)
throw new RpcException(new Status(StatusCode.InvalidArgument, error));
}
private async ValueTask AddSubscriptionAsync(GrpcWorkerConnection connection, AddSubscriptionRequest request)
{
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()
{
AddSubscriptionResponse = 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<string> agentTypes, CloudEvent evt)
{
var tasks = new List<Task>(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);
}
async ValueTask<RpcResponse> IGateway.InvokeRequest(RpcRequest request)
public async ValueTask BroadcastEventAsync(CloudEvent evt, CancellationToken cancellationToken = default)
{
return await this.InvokeRequest(request).ConfigureAwait(false);
}
var tasks = new List<Task>(_workers.Count);
foreach (var (_, connection) in _supportedAgentTypes)
{
tasks.Add(this.SendMessageAsync((IConnection)connection[0], evt, default));
}
await Task.WhenAll(tasks).ConfigureAwait(false);
}
Task IGateway.SendMessageAsync(IConnection connection, CloudEvent cloudEvent)
{
return this.SendMessageAsync(connection, cloudEvent);
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);
}
public async ValueTask<RemoveSubscriptionResponse> UnsubscribeAsync(RemoveSubscriptionRequest request, CancellationToken cancellationToken = default)
{
try
{
await _gatewayRegistry.UnsubscribeAsync(request).ConfigureAwait(true);
return new RemoveSubscriptionResponse
{
Success = true,
};
}
catch (Exception ex)
{
return new RemoveSubscriptionResponse
{
Success = false,
Error = ex.Message
};
}
}
public ValueTask<List<Subscription>> GetSubscriptionsAsync(GetSubscriptionsRequest request, CancellationToken cancellationToken = default)
{
return _gatewayRegistry.GetSubscriptions(request);
}
async ValueTask<RpcResponse> 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<AgentState> IGateway.ReadAsync(AgentId agentId)
{
return ReadAsync(agentId, default);
}
ValueTask<RegisterAgentTypeResponse> IGateway.RegisterAgentTypeAsync(RegisterAgentTypeRequest request)
{
return RegisterAgentTypeAsync(request, default);
}
ValueTask<AddSubscriptionResponse> IGateway.SubscribeAsync(AddSubscriptionRequest request)
{
return SubscribeAsync(request, default);
}
ValueTask<RemoveSubscriptionResponse> IGateway.UnsubscribeAsync(RemoveSubscriptionRequest request)
{
return UnsubscribeAsync(request, default);
}
ValueTask<List<Subscription>> IGateway.GetSubscriptionsAsync(GetSubscriptionsRequest request)
{
return GetSubscriptionsAsync(request);
}
}

View File

@ -7,13 +7,10 @@ using Microsoft.AutoGen.Contracts;
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(GrpcGateway gateway) : AgentRpc.AgentRpcBase
{
private readonly GrpcGateway Gateway;
public GrpcGatewayService(GrpcGateway gateway)
{
Gateway = (GrpcGateway)gateway;
}
private readonly GrpcGateway Gateway = (GrpcGateway)gateway;
public override async Task OpenChannel(IAsyncStreamReader<Message> requestStream, IServerStreamWriter<Message> responseStream, ServerCallContext context)
{
try
@ -34,7 +31,6 @@ internal sealed class GrpcGatewayService : AgentRpc.AgentRpcBase
var state = await Gateway.ReadAsync(request);
return new GetStateResponse { AgentState = state };
}
public override async Task<SaveStateResponse> SaveState(AgentState request, ServerCallContext context)
{
await Gateway.StoreAsync(request);
@ -43,4 +39,23 @@ internal sealed class GrpcGatewayService : AgentRpc.AgentRpcBase
Success = true // TODO: Implement error handling
};
}
public override async Task<AddSubscriptionResponse> AddSubscription(AddSubscriptionRequest request, ServerCallContext context)
{
request.RequestId = context.Peer;
return await Gateway.SubscribeAsync(request).ConfigureAwait(true);
}
public override async Task<RemoveSubscriptionResponse> RemoveSubscription(RemoveSubscriptionRequest request, ServerCallContext context)
{
return await Gateway.UnsubscribeAsync(request).ConfigureAwait(true);
}
public override async Task<GetSubscriptionsResponse> GetSubscriptions(GetSubscriptionsRequest request, ServerCallContext context)
{
var subscriptions = await Gateway.GetSubscriptionsAsync(request);
return new GetSubscriptionsResponse { Subscriptions = { subscriptions } };
}
public override async Task<RegisterAgentTypeResponse> RegisterAgent(RegisterAgentTypeRequest request, ServerCallContext context)
{
request.RequestId = context.Peer;
return await Gateway.RegisterAgentTypeAsync(request).ConfigureAwait(true);
}
}

View File

@ -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<string> _supportedTypes = [];
private readonly GrpcGateway _gateway;
private readonly CancellationTokenSource _shutdownCancellationToken = new();
public Task Completion { get; private set; } = Task.CompletedTask;
public GrpcWorkerConnection(GrpcGateway agentWorker, IAsyncStreamReader<Message> requestStream, IServerStreamWriter<Message> responseStream, ServerCallContext context)
{
_gateway = agentWorker;
@ -25,7 +25,9 @@ internal sealed class GrpcWorkerConnection : IAsyncDisposable, IConnection
ResponseStream = responseStream;
ServerCallContext = context;
_outboundMessages = Channel.CreateUnbounded<Message>(new UnboundedChannelOptions { AllowSynchronousContinuations = true, SingleReader = true, SingleWriter = false });
}
public Task Connect()
{
var didSuppress = false;
if (!ExecutionContext.IsFlowSuppressed())
{
@ -46,7 +48,7 @@ internal sealed class GrpcWorkerConnection : IAsyncDisposable, IConnection
}
}
Completion = Task.WhenAll(_readTask, _writeTask);
return Completion = Task.WhenAll(_readTask, _writeTask);
}
public IAsyncStreamReader<Message> RequestStream { get; }
@ -75,9 +77,6 @@ internal sealed class GrpcWorkerConnection : IAsyncDisposable, IConnection
{
await _outboundMessages.Writer.WriteAsync(message).ConfigureAwait(false);
}
public Task Completion { get; }
public async Task RunReadPump()
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
@ -85,9 +84,8 @@ internal sealed class GrpcWorkerConnection : IAsyncDisposable, IConnection
{
await foreach (var message in RequestStream.ReadAllAsync(_shutdownCancellationToken.Token))
{
// Fire and forget
_gateway.OnReceivedMessageAsync(this, message).Ignore();
_gateway.OnReceivedMessageAsync(this, message, _shutdownCancellationToken.Token).Ignore();
}
}
catch (OperationCanceledException)

View File

@ -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<RpcResponse> InvokeRequest(RpcRequest request);
ValueTask BroadcastEvent(CloudEvent evt);
ValueTask StoreAsync(AgentState value);
ValueTask<AgentState> ReadAsync(AgentId agentId);
Task SendMessageAsync(IConnection connection, CloudEvent cloudEvent);
}

View File

@ -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<AgentState> state) : Grain, IAgentState
internal sealed class AgentStateGrain([PersistentState("state", "AgentStateStore")] IPersistentState<AgentState> state) : Grain, IAgentState, IAgentGrain
{
/// <inheritdoc />
public async ValueTask<string> WriteStateAsync(AgentState newState, string eTag, CancellationToken cancellationToken = default)
@ -33,4 +34,14 @@ internal sealed class AgentStateGrain([PersistentState("state", "AgentStateStore
{
return ValueTask.FromResult(state.State);
}
ValueTask<AgentState> IAgentGrain.ReadStateAsync()
{
return ReadStateAsync();
}
ValueTask<string> IAgentGrain.WriteStateAsync(AgentState state, string eTag)
{
return WriteStateAsync(state, eTag);
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// AgentsRegistryState.cs
using Microsoft.AutoGen.Contracts;
namespace Microsoft.AutoGen.Runtime.Grpc;
public class AgentsRegistryState
{
public Dictionary<string, HashSet<string>> AgentsToEventsMap { get; set; } = [];
public Dictionary<string, HashSet<string>> AgentsToTopicsMap { get; set; } = [];
public Dictionary<string, HashSet<string>> TopicToAgentTypesMap { get; set; } = [];
public Dictionary<string, HashSet<string>> EventsToAgentTypesMap { get; set; } = [];
public Dictionary<string, HashSet<Subscription>> GuidSubscriptionsMap { get; set; } = [];
}

View File

@ -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<IGateway?> GetCompatibleWorker(string type);
}

View File

@ -16,7 +16,6 @@ public static class OrleansRuntimeHostingExtenions
public static WebApplicationBuilder AddOrleans(this WebApplicationBuilder builder)
{
builder.Services.AddSerializer(serializer => serializer.AddProtobufSerializer());
builder.Services.AddSingleton<IRegistryGrain, RegistryGrain>();
// 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 class OrleansRuntimeHostingExtenions
siloBuilder.UseLocalhostClustering()
.AddMemoryStreams("StreamProvider")
.AddMemoryGrainStorage("PubSubStore")
.AddMemoryGrainStorage("AgentRegistryStore")
.AddMemoryGrainStorage("AgentStateStore");
siloBuilder.UseInMemoryReminderService();
@ -40,12 +40,7 @@ public static class OrleansRuntimeHostingExtenions
var cosmosDbconnectionString = builder.Configuration.GetValue<string>("Orleans:CosmosDBConnectionString") ??
throw new ConfigurationErrorsException(
"Orleans:CosmosDBConnectionString is missing from configuration. This is required for persistence in production environments.");
siloBuilder.Configure<ClusterOptions>(options =>
{
//TODO: make this configurable
options.ClusterId = "AutoGen-cluster";
options.ServiceId = "AutoGen-cluster";
});
siloBuilder.Configure<SiloMessagingOptions>(options =>
{
options.ResponseTimeout = TimeSpan.FromMinutes(3);

View File

@ -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<AgentsRegistryState> state) : Grain, IRegistryGrain
{
// TODO: use persistent state for some of these or (better) extend Orleans to implement some of this natively.
private readonly Dictionary<IGateway, WorkerState> _workerStates = new();
@ -18,9 +18,48 @@ internal sealed class RegistryGrain : Grain, IRegistryGrain
this.RegisterGrainTimer(static state => state.PurgeInactiveWorkers(), this, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
return base.OnActivateAsync(cancellationToken);
}
public ValueTask<List<string>> GetSubscribedAndHandlingAgents(string topic, string eventType)
{
List<string> agents = [];
// get all agent types that are subscribed to the topic
if (state.State.TopicToAgentTypesMap.TryGetValue(topic, out var subscribedAgentTypes))
{
/*// get all agent types that are handling the event
if (state.State.EventsToAgentTypesMap.TryGetValue(eventType, out var handlingAgents))
{
agents.AddRange(subscribedAgentTypes.Intersect(handlingAgents).ToList());
}*/
agents.AddRange(subscribedAgentTypes.ToList());
}
if (state.State.TopicToAgentTypesMap.TryGetValue(eventType, out var eventHandlingAgents))
{
agents.AddRange(eventHandlingAgents.ToList());
}
if (state.State.TopicToAgentTypesMap.TryGetValue(topic + "." + eventType, out var combo))
{
agents.AddRange(combo.ToList());
}
// instead of an exact match, we can also check for a prefix match where key starts with the eventType
if (state.State.TopicToAgentTypesMap.Keys.Any(key => key.StartsWith(eventType)))
{
state.State.TopicToAgentTypesMap.Where(
kvp => kvp.Key.StartsWith(eventType))
.SelectMany(kvp => kvp.Value)
.Distinct()
.ToList()
.ForEach(async agentType =>
{
agents.Add(agentType);
});
}
agents = agents.Distinct().ToList();
return new ValueTask<List<string>>(agents);
}
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 +97,49 @@ internal sealed class RegistryGrain : Grain, IRegistryGrain
}
return ValueTask.CompletedTask;
}
public ValueTask RegisterAgentType(string type, IGateway worker)
public async ValueTask RegisterAgentType(RegisterAgentTypeRequest registration, IGateway gateway)
{
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))
if (!supportedAgentTypes.Contains(gateway))
{
supportedAgentTypes.Add(worker);
supportedAgentTypes.Add(gateway);
}
var workerState = GetOrAddWorker(worker);
workerState.SupportedTypes.Add(type);
return ValueTask.CompletedTask;
var workerState = GetOrAddWorker(gateway);
workerState.SupportedTypes.Add(registration.Type);
/* future
state.State.AgentsToEventsMap[registration.Type] = new HashSet<string>(registration.Events);
state.State.AgentsToTopicsMap[registration.Type] = new HashSet<string>(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<string>();
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<string>();
state.State.EventsToAgentTypesMap[evt] = eventSet;
}
eventSet.Add(registration.Type);
}
*/
await state.WriteStateAsync().ConfigureAwait(false);
}
public ValueTask AddWorker(IGateway worker)
{
@ -135,9 +203,123 @@ internal sealed class RegistryGrain : Grain, IRegistryGrain
return null;
}
public async ValueTask SubscribeAsync(AddSubscriptionRequest subscription)
{
var guid = Guid.NewGuid().ToString();
subscription.Subscription.Id = guid;
switch (subscription.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(subscription.Subscription.TypeSubscription.AgentType, out var topics);
if (topics is null)
{
topics = new HashSet<string>();
state.State.AgentsToTopicsMap[subscription.Subscription.TypeSubscription.AgentType] = topics;
}
topics.Add(subscription.Subscription.TypeSubscription.TopicType);
// add the agent type to the set of agent types for the topic
state.State.TopicToAgentTypesMap.TryGetValue(subscription.Subscription.TypeSubscription.TopicType, out var agents);
if (agents is null)
{
agents = new HashSet<string>();
state.State.TopicToAgentTypesMap[subscription.Subscription.TypeSubscription.TopicType] = agents;
}
agents.Add(subscription.Subscription.TypeSubscription.AgentType);
// add the subscription by Guid
state.State.GuidSubscriptionsMap.TryGetValue(guid, out var existingSubscriptions);
if (existingSubscriptions is null)
{
existingSubscriptions = new HashSet<Subscription>();
state.State.GuidSubscriptionsMap[guid] = existingSubscriptions;
}
existingSubscriptions.Add(subscription.Subscription);
break;
}
default:
throw new InvalidOperationException("Invalid subscription type");
}
await state.WriteStateAsync().ConfigureAwait(false);
}
public async ValueTask UnsubscribeAsync(RemoveSubscriptionRequest request)
{
var guid = request.Id;
// does the guid parse?
if (!Guid.TryParse(guid, out var _))
{
throw new InvalidOperationException("Invalid subscription id");
}
if (state.State.GuidSubscriptionsMap.TryGetValue(guid, out var subscriptions))
{
foreach (var subscription in subscriptions)
{
switch (subscription.SubscriptionCase)
{
case Subscription.SubscriptionOneofCase.TypeSubscription:
{
// remove the topic from the set of topics for the agent type
state.State.AgentsToTopicsMap.TryGetValue(subscription.TypeSubscription.AgentType, out var topics);
topics?.Remove(subscription.TypeSubscription.TopicType);
// remove the agent type from the set of agent types for the topic
state.State.TopicToAgentTypesMap.TryGetValue(subscription.TypeSubscription.TopicType, out var agents);
agents?.Remove(subscription.TypeSubscription.AgentType);
//remove the subscription by Guid
state.State.GuidSubscriptionsMap.TryGetValue(guid, out var existingSubscriptions);
existingSubscriptions?.Remove(subscription);
break;
}
case Subscription.SubscriptionOneofCase.TypePrefixSubscription:
break;
default:
throw new InvalidOperationException("Invalid subscription type");
}
}
state.State.GuidSubscriptionsMap.Remove(guid);
}
await state.WriteStateAsync().ConfigureAwait(false);
}
public ValueTask<List<Subscription>> GetSubscriptions(string agentType)
{
var subscriptions = new List<Subscription>();
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);
}
public ValueTask<List<Subscription>> GetSubscriptions(GetSubscriptionsRequest request)
{
var subscriptions = new List<Subscription>();
foreach (var kvp in state.State.GuidSubscriptionsMap)
{
subscriptions.AddRange(kvp.Value);
}
return new(subscriptions);
}
private sealed class WorkerState
{
public HashSet<string> SupportedTypes { get; set; } = [];
public DateTimeOffset LastSeen { get; set; }
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// AddSubscriptionRequestSurrogate.cs
using Microsoft.AutoGen.Contracts;
namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates;
[GenerateSerializer]
public struct AddSubscriptionRequestSurrogate
{
[Id(0)]
public string RequestId;
[Id(1)]
public Subscription Subscription;
}
[RegisterConverter]
public sealed class AddSubscriptionRequestSurrogateConverter :
IConverter<AddSubscriptionRequest, AddSubscriptionRequestSurrogate>
{
public AddSubscriptionRequest ConvertFromSurrogate(
in AddSubscriptionRequestSurrogate surrogate)
{
var request = new AddSubscriptionRequest()
{
RequestId = surrogate.RequestId,
Subscription = surrogate.Subscription
};
return request;
}
public AddSubscriptionRequestSurrogate ConvertToSurrogate(
in AddSubscriptionRequest value) =>
new AddSubscriptionRequestSurrogate
{
RequestId = value.RequestId,
Subscription = value.Subscription
};
}

View File

@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// AddSubscriptionResponseSurrogate.cs
using Microsoft.AutoGen.Contracts;
namespace Microsoft.AutoGen.Runtime.Grpc.Orleans.Surrogates;
[GenerateSerializer]
public struct AddSubscriptionResponseSurrogate
{
[Id(0)]
public string RequestId;
[Id(1)]
public bool Success;
[Id(2)]
public string Error;
}
[RegisterConverter]
public sealed class AddSubscriptionResponseSurrogateConverter :
IConverter<AddSubscriptionResponse, AddSubscriptionResponseSurrogate>
{
public AddSubscriptionResponse ConvertFromSurrogate(
in AddSubscriptionResponseSurrogate surrogate) =>
new AddSubscriptionResponse
{
RequestId = surrogate.RequestId,
Success = surrogate.Success,
Error = surrogate.Error
};
public AddSubscriptionResponseSurrogate ConvertToSurrogate(
in AddSubscriptionResponse value) =>
new AddSubscriptionResponseSurrogate
{
RequestId = value.RequestId,
Success = value.Success,
Error = value.Error
};
}

View File

@ -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<AgentId, AgentIdSurrogate>
{
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
};
}

Some files were not shown because too many files have changed in this diff Show More