mirror of https://github.com/microsoft/autogen.git
Initial cross-language protocol for agents (#139)
* Initial prototype of .NET gRPC worker client + service --------- Co-authored-by: Jack Gerrits <jack@jackgerrits.com>
This commit is contained in:
parent
13b0d0deb4
commit
ebed669231
|
@ -38,6 +38,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AI.Agents.Worker.Client", "src\Microsoft.AI.Agents.Worker.Client\Microsoft.AI.Agents.Worker.Client.csproj", "{20E5C8C3-CE40-4FC3-96F8-B4A2C51936E9}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AI.Agents.Worker.Server", "src\Microsoft.AI.Agents.Worker.Server\Microsoft.AI.Agents.Worker.Server.csproj", "{B9188ADC-D322-4B38-B3D6-95338E89C34B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "greeter", "greeter", "{320B05A6-4E1B-4B15-B3F6-745819D2BF22}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greeter.AppHost", "samples\Greeter\Greeter.AppHost\Greeter.AppHost.csproj", "{06B30F2A-BA17-451A-81FF-E9CC9551F671}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greeter.ServiceDefaults", "samples\Greeter\Greeter.ServiceDefaults\Greeter.ServiceDefaults.csproj", "{E45990FD-85B3-44A2-8646-4AB2E868BC5F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greeter.AgentHost", "samples\Greeter\Greeter.AgentHost\Greeter.AgentHost.csproj", "{590BACCE-7310-4D7B-9618-46496F2EB171}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greeter.AgentWorker", "samples\Greeter\Greeter.AgentWorker\Greeter.AgentWorker.csproj", "{7BA721F2-EE46-4A85-A8C8-3695C4ADF93E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -76,6 +90,30 @@ Global
|
|||
{92CAAA29-8633-4984-B169-29BB89E0EA23}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{92CAAA29-8633-4984-B169-29BB89E0EA23}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{92CAAA29-8633-4984-B169-29BB89E0EA23}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{20E5C8C3-CE40-4FC3-96F8-B4A2C51936E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{20E5C8C3-CE40-4FC3-96F8-B4A2C51936E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{20E5C8C3-CE40-4FC3-96F8-B4A2C51936E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{20E5C8C3-CE40-4FC3-96F8-B4A2C51936E9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B9188ADC-D322-4B38-B3D6-95338E89C34B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B9188ADC-D322-4B38-B3D6-95338E89C34B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B9188ADC-D322-4B38-B3D6-95338E89C34B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B9188ADC-D322-4B38-B3D6-95338E89C34B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{06B30F2A-BA17-451A-81FF-E9CC9551F671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{06B30F2A-BA17-451A-81FF-E9CC9551F671}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{06B30F2A-BA17-451A-81FF-E9CC9551F671}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{06B30F2A-BA17-451A-81FF-E9CC9551F671}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E45990FD-85B3-44A2-8646-4AB2E868BC5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E45990FD-85B3-44A2-8646-4AB2E868BC5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E45990FD-85B3-44A2-8646-4AB2E868BC5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E45990FD-85B3-44A2-8646-4AB2E868BC5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{590BACCE-7310-4D7B-9618-46496F2EB171}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{590BACCE-7310-4D7B-9618-46496F2EB171}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{590BACCE-7310-4D7B-9618-46496F2EB171}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{590BACCE-7310-4D7B-9618-46496F2EB171}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7BA721F2-EE46-4A85-A8C8-3695C4ADF93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7BA721F2-EE46-4A85-A8C8-3695C4ADF93E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7BA721F2-EE46-4A85-A8C8-3695C4ADF93E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7BA721F2-EE46-4A85-A8C8-3695C4ADF93E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -94,6 +132,13 @@ Global
|
|||
{EF5DF177-F4F2-49D5-9E1C-2E37869238D8} = {943853E7-513D-45EA-870F-549CFC0AF8E8}
|
||||
{92CAAA29-8633-4984-B169-29BB89E0EA23} = {65CF8F20-D740-46AC-A869-FA609D960A09}
|
||||
{65CF8F20-D740-46AC-A869-FA609D960A09} = {943853E7-513D-45EA-870F-549CFC0AF8E8}
|
||||
{20E5C8C3-CE40-4FC3-96F8-B4A2C51936E9} = {290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF}
|
||||
{B9188ADC-D322-4B38-B3D6-95338E89C34B} = {290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF}
|
||||
{320B05A6-4E1B-4B15-B3F6-745819D2BF22} = {943853E7-513D-45EA-870F-549CFC0AF8E8}
|
||||
{06B30F2A-BA17-451A-81FF-E9CC9551F671} = {320B05A6-4E1B-4B15-B3F6-745819D2BF22}
|
||||
{E45990FD-85B3-44A2-8646-4AB2E868BC5F} = {320B05A6-4E1B-4B15-B3F6-745819D2BF22}
|
||||
{590BACCE-7310-4D7B-9618-46496F2EB171} = {320B05A6-4E1B-4B15-B3F6-745819D2BF22}
|
||||
{7BA721F2-EE46-4A85-A8C8-3695C4ADF93E} = {320B05A6-4E1B-4B15-B3F6-745819D2BF22}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C9250809-2B94-4552-99DE-333E4646A16F}
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="8.0.0" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="8.0.1-preview.8.24267.1" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="8.0.1" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.ApplicationInsights" Version="8.0.1" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="8.0.1" />
|
||||
<PackageVersion Include="Aspire.Hosting.NodeJs" Version="8.0.1" />
|
||||
<PackageVersion Include="Aspire.Hosting.Orleans" Version="8.0.1" />
|
||||
<PackageVersion Include="Aspire.Hosting.Qdrant" Version="8.0.1" />
|
||||
<PackageVersion Include="Aspire.Hosting.Redis" Version="8.0.1" />
|
||||
<PackageVersion Include="Azure.Data.Tables" Version="12.8.1" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.12.0" />
|
||||
<PackageVersion Include="Azure.ResourceManager.ContainerInstance" Version="1.2.0" />
|
||||
|
@ -23,6 +31,9 @@
|
|||
<PackageVersion Include="Elsa.Workflows.Core" Version="3.0.6" />
|
||||
<PackageVersion Include="Elsa.Workflows.Designer" Version="3.0.0-preview.727" />
|
||||
<PackageVersion Include="Elsa.Workflows.Management" Version="3.0.6" />
|
||||
<PackageVersion Include="Grpc.AspNetCore" Version="2.63.0" />
|
||||
<PackageVersion Include="Grpc.Net.ClientFactory" Version="2.63.0" />
|
||||
<PackageVersion Include="Grpc.Tools" Version="2.63.0" />
|
||||
<PackageVersion Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Azure" Version="1.7.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
|
@ -34,12 +45,14 @@
|
|||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.6.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Clustering.Cosmos" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Persistence.Cosmos" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Reminders" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Reminders.Cosmos" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Runtime" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Sdk" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Serialization.Protobuf" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Server" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Streaming" Version="8.1.0" />
|
||||
<PackageVersion Include="Microsoft.Orleans.Streaming.EventHubs" Version="8.1.0" />
|
||||
|
@ -49,6 +62,11 @@
|
|||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Octokit" Version="10.0.0" />
|
||||
<PackageVersion Include="Octokit.Webhooks.AspNetCore" Version="2.0.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />
|
||||
<PackageVersion Include="OrleansDashboard" Version="8.0.0" />
|
||||
<PackageVersion Include="PdfPig" Version="0.1.9-alpha-20240324-e7896" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.AI.Agents.Worker.Server\Microsoft.AI.Agents.Worker.Server.csproj" />
|
||||
<ProjectReference Include="..\Greeter.ServiceDefaults\Greeter.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Orleans.Server" />
|
||||
<PackageReference Include="Microsoft.Orleans.Reminders" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,17 @@
|
|||
using Microsoft.AI.Agents.Worker;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.AddServiceDefaults();
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddGrpc();
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Information);
|
||||
|
||||
builder.AddAgentService();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapAgentService();
|
||||
app.UseExceptionHandler();
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
app.Run();
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5438",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7511;http://localhost:5438",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
"Protocols": "Http2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using Agents;
|
||||
using Microsoft.AI.Agents.Worker.Client;
|
||||
using AgentId = Microsoft.AI.Agents.Worker.Client.AgentId;
|
||||
|
||||
namespace Greeter.AgentWorker;
|
||||
|
||||
internal sealed class Client(ILogger<Client> logger, AgentWorkerRuntime runtime) : AgentBase(new ClientContext(logger, runtime))
|
||||
{
|
||||
private sealed class ClientContext(ILogger<Client> logger, AgentWorkerRuntime runtime) : IAgentContext
|
||||
{
|
||||
public AgentId AgentId { get; } = new AgentId("client", Guid.NewGuid().ToString());
|
||||
public AgentBase? AgentInstance { get; set; }
|
||||
public ILogger Logger { get; } = logger;
|
||||
|
||||
public async ValueTask PublishEventAsync(Event @event)
|
||||
{
|
||||
await runtime.PublishEvent(@event).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask SendRequestAsync(AgentBase agent, RpcRequest request)
|
||||
{
|
||||
await runtime.SendRequest(AgentInstance!, request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask SendResponseAsync(RpcRequest request, RpcResponse response)
|
||||
{
|
||||
await runtime.SendResponse(response).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Greeter.ServiceDefaults\Greeter.ServiceDefaults.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.AI.Agents.Worker.Client\Microsoft.AI.Agents.Worker.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,62 @@
|
|||
using Agents;
|
||||
using Greeter.AgentWorker;
|
||||
using Microsoft.AI.Agents.Worker.Client;
|
||||
using AgentId = Microsoft.AI.Agents.Worker.Client.AgentId;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add service defaults & Aspire components.
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
var agentBuilder = builder.AddAgentWorker("https://agenthost");
|
||||
agentBuilder.AddAgent<GreetingAgent>("greeter");
|
||||
builder.Services.AddHostedService<MyBackgroundService>();
|
||||
builder.Services.AddSingleton<Client>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
internal sealed class GreetingAgent(IAgentContext context, ILogger<GreetingAgent> logger) : AgentBase(context)
|
||||
{
|
||||
protected override Task HandleEvent(Microsoft.AI.Agents.Abstractions.Event @event)
|
||||
{
|
||||
logger.LogInformation("[{Id}] Received event: '{Event}'.", AgentId, @event);
|
||||
return base.HandleEvent(@event);
|
||||
}
|
||||
|
||||
protected override Task<RpcResponse> HandleRequest(RpcRequest request)
|
||||
{
|
||||
logger.LogInformation("[{Id}] Received request: '{Request}'.", AgentId, request);
|
||||
return Task.FromResult(new RpcResponse() { Result = "Okay!" });
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MyBackgroundService(ILogger<MyBackgroundService> logger, Client client) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var generatedCodeId = Guid.NewGuid().ToString();
|
||||
var instanceId = Guid.NewGuid().ToString();
|
||||
var response = await client.RequestAsync(
|
||||
new AgentId("greeter", "foo"),
|
||||
"echo",
|
||||
new Dictionary<string, string> { ["message"] = "Hello, agents!" }).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Received response: {Response}", response);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Error invoking request.");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5181",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7050;http://localhost:5181",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireHost>true</IsAspireHost>
|
||||
<UserSecretsId>6e251df6-43b1-498f-87a8-3cc77c302c21</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Greeter.AgentHost\Greeter.AgentHost.csproj" />
|
||||
<ProjectReference Include="..\Greeter.AgentWorker\Greeter.AgentWorker.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" />
|
||||
<PackageReference Include="Aspire.Hosting.Azure.ApplicationInsights" />
|
||||
<PackageReference Include="Aspire.Hosting.Azure.CognitiveServices" />
|
||||
<PackageReference Include="Aspire.Hosting.NodeJs" />
|
||||
<PackageReference Include="Aspire.Hosting.Orleans" />
|
||||
<PackageReference Include="Aspire.Hosting.Qdrant" />
|
||||
<PackageReference Include="Aspire.Hosting.Redis" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,21 @@
|
|||
var builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddAzureProvisioning();
|
||||
|
||||
var orleans = builder.AddOrleans("orleans")
|
||||
.WithDevelopmentClustering()
|
||||
.WithMemoryReminders()
|
||||
.WithMemoryGrainStorage("agent-state");
|
||||
|
||||
var agentHost = builder.AddProject<Projects.Greeter_AgentHost>("agenthost")
|
||||
.WithReference(orleans);
|
||||
|
||||
builder.AddProject<Projects.Greeter_AgentWorker>("csharp-worker")
|
||||
.WithExternalHttpEndpoints()
|
||||
.WithReference(agentHost);
|
||||
|
||||
var ep = agentHost.GetEndpoint("http");
|
||||
builder.AddExecutable("python-worker", "hatch", "../../../../python/", "run", "python", "worker_example.py")
|
||||
.WithEnvironment("AGENT_HOST", $"{ep.Property(EndpointProperty.Host)}:{ep.Property(EndpointProperty.Port)}");
|
||||
|
||||
builder.Build().Run();
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:17267;http://localhost:15190",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21107",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22230"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:15190",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19078",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20236"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
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 IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
|
||||
{
|
||||
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();
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
|
||||
{
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.IncludeFormattedMessage = true;
|
||||
logging.IncludeScopes = true;
|
||||
});
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation();
|
||||
})
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing.AddAspNetCoreInstrumentation()
|
||||
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
|
||||
//.AddGrpcClientInstrumentation()
|
||||
.AddHttpClientInstrumentation();
|
||||
});
|
||||
|
||||
builder.AddOpenTelemetryExporters();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
|
||||
{
|
||||
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 IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -24,8 +24,9 @@ public class AzureGenie : Agent, IDaprAgent
|
|||
{
|
||||
var context = item.ToGithubContext();
|
||||
await Store(context.Org, context.Repo, context.ParentNumber ?? 0, context.IssueNumber, "readme", "md", "output", item.Data["readme"]);
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.ReadmeStored),
|
||||
Subject = context.Subject,
|
||||
Data = context.ToData()
|
||||
|
@ -38,8 +39,9 @@ public class AzureGenie : Agent, IDaprAgent
|
|||
var context = item.ToGithubContext();
|
||||
await Store(context.Org, context.Repo, context.ParentNumber ?? 0, context.IssueNumber, "run", "sh", "output", item.Data["code"]);
|
||||
await RunInSandbox(context.Org, context.Repo, context.ParentNumber ?? 0, context.IssueNumber);
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.SandboxRunCreated),
|
||||
Subject = context.Subject,
|
||||
Data = context.ToData()
|
||||
|
|
|
@ -27,8 +27,9 @@ public class Dev : AiAgent<DeveloperState>, IDaprAgent
|
|||
var code = await GenerateCode(item.Data["input"]);
|
||||
var data = context.ToData();
|
||||
data["result"] = code;
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.CodeGenerated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
@ -41,8 +42,9 @@ public class Dev : AiAgent<DeveloperState>, IDaprAgent
|
|||
var lastCode = state.History.Last().Message;
|
||||
var data = context.ToData();
|
||||
data["code"] = lastCode;
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.CodeCreated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
|
|
@ -28,8 +28,9 @@ public class DeveloperLead : AiAgent<DeveloperLeadState>, IDaprAgent
|
|||
var plan = await CreatePlan(item.Data["input"]);
|
||||
var data = context.ToData();
|
||||
data["result"] = plan;
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.DevPlanGenerated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
@ -42,8 +43,9 @@ public class DeveloperLead : AiAgent<DeveloperLeadState>, IDaprAgent
|
|||
var latestPlan = state.History.Last().Message;
|
||||
var data = context.ToData();
|
||||
data["plan"] = latestPlan;
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.DevPlanCreated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
|
|
@ -29,8 +29,9 @@ public class ProductManager : AiAgent<ProductManagerState>, IDaprAgent
|
|||
var readme = await CreateReadme(item.Data["input"]);
|
||||
var data = context.ToData();
|
||||
data["result"] = readme;
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.ReadmeGenerated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
@ -43,8 +44,9 @@ public class ProductManager : AiAgent<ProductManagerState>, IDaprAgent
|
|||
var lastReadme = state.History.Last().Message;
|
||||
var data = context.ToData();
|
||||
data["readme"] = lastReadme;
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.ReadmeCreated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
|
|
@ -84,8 +84,9 @@ public class Sandbox : Agent, IDaprAgent, IRemindable
|
|||
{ "parentNumber", agentState.ParentIssueNumber.ToString() }
|
||||
};
|
||||
var subject = $"{agentState.Org}-{agentState.Repo}-{agentState.IssueNumber}";
|
||||
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = Consts.MainTopic,
|
||||
Type = nameof(GithubFlowEventType.SandboxRunFinished),
|
||||
Subject = subject,
|
||||
Data = data
|
||||
|
|
|
@ -117,6 +117,7 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor
|
|||
|
||||
var evt = new Event
|
||||
{
|
||||
Namespace = subject,
|
||||
Type = eventType,
|
||||
Subject = subject,
|
||||
Data = data
|
||||
|
@ -148,6 +149,7 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor
|
|||
};
|
||||
var evt = new Event
|
||||
{
|
||||
Namespace = subject,
|
||||
Type = eventType,
|
||||
Subject = subject,
|
||||
Data = data
|
||||
|
|
|
@ -28,8 +28,9 @@ public class AzureGenie : Agent
|
|||
{
|
||||
var context = item.ToGithubContext();
|
||||
await Store(context.Org, context.Repo, context.ParentNumber ?? 0, context.IssueNumber, "readme", "md", "output", item.Data["readme"]);
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.ReadmeStored),
|
||||
Subject = context.Subject,
|
||||
Data = context.ToData()
|
||||
|
@ -42,8 +43,9 @@ public class AzureGenie : Agent
|
|||
var context = item.ToGithubContext();
|
||||
await Store(context.Org, context.Repo, context.ParentNumber ?? 0, context.IssueNumber, "run", "sh", "output", item.Data["code"]);
|
||||
await RunInSandbox(context.Org, context.Repo, context.ParentNumber ?? 0, context.IssueNumber);
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.SandboxRunCreated),
|
||||
Subject = context.Subject,
|
||||
Data = context.ToData()
|
||||
|
|
|
@ -31,8 +31,9 @@ public class Dev : AiAgent<DeveloperState>, IDevelopApps
|
|||
var code = await GenerateCode(item.Data["input"]);
|
||||
var data = context.ToData();
|
||||
data["result"] = code;
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.CodeGenerated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
@ -46,8 +47,9 @@ public class Dev : AiAgent<DeveloperState>, IDevelopApps
|
|||
var lastCode = _state.State.History.Last().Message;
|
||||
var data = context.ToData();
|
||||
data["code"] = lastCode;
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.CodeCreated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
|
|
@ -31,8 +31,9 @@ public class DeveloperLead : AiAgent<DeveloperLeadState>, ILeadDevelopers
|
|||
var plan = await CreatePlan(item.Data["input"]);
|
||||
var data = context.ToData();
|
||||
data["result"] = plan;
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.DevPlanGenerated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
@ -46,8 +47,9 @@ public class DeveloperLead : AiAgent<DeveloperLeadState>, ILeadDevelopers
|
|||
var latestPlan = _state.State.History.Last().Message;
|
||||
var data = context.ToData();
|
||||
data["plan"] = latestPlan;
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.DevPlanCreated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
|
|
@ -30,8 +30,9 @@ public class ProductManager : AiAgent<ProductManagerState>, IManageProducts
|
|||
var readme = await CreateReadme(item.Data["input"]);
|
||||
var data = context.ToData();
|
||||
data["result"] = readme;
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.ReadmeGenerated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
@ -45,8 +46,9 @@ public class ProductManager : AiAgent<ProductManagerState>, IManageProducts
|
|||
var lastReadme = _state.State.History.Last().Message;
|
||||
var data = context.ToData();
|
||||
data["readme"] = lastReadme;
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.ReadmeCreated),
|
||||
Subject = context.Subject,
|
||||
Data = data
|
||||
|
|
|
@ -58,14 +58,16 @@ public sealed class Sandbox : Agent, IRemindable
|
|||
if (await _azService.IsSandboxCompleted(sandboxId))
|
||||
{
|
||||
await _azService.DeleteSandbox(sandboxId);
|
||||
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(GithubFlowEventType.SandboxRunFinished),
|
||||
Data = new Dictionary<string, string> {
|
||||
{ "org", _state.State.Org },
|
||||
{ "repo", _state.State.Repo },
|
||||
{ "issueNumber", _state.State.IssueNumber.ToString() },
|
||||
{ "parentNumber", _state.State.ParentIssueNumber.ToString() }
|
||||
Data = new Dictionary<string, string>
|
||||
{
|
||||
["org"] = _state.State.Org,
|
||||
["repo"] = _state.State.Repo,
|
||||
["issueNumber"] = _state.State.IssueNumber.ToString(),
|
||||
["parentNumber"] = _state.State.ParentIssueNumber.ToString()
|
||||
}
|
||||
});
|
||||
await Cleanup();
|
||||
|
|
|
@ -117,7 +117,7 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor
|
|||
{
|
||||
var subject = suffix + issueNumber.ToString();
|
||||
var streamProvider = _client.GetStreamProvider("StreamProvider");
|
||||
var streamId = StreamId.Create(Consts.MainNamespace, subject);
|
||||
var streamId = StreamId.Create(ns: "default", key: subject);
|
||||
var stream = streamProvider.GetStream<Event>(streamId);
|
||||
var eventType = (skillName, functionName) switch
|
||||
{
|
||||
|
@ -128,14 +128,15 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor
|
|||
};
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
{ "org", org },
|
||||
{ "repo", repo },
|
||||
{ "issueNumber", issueNumber.ToString() },
|
||||
{ "parentNumber", (parentNumber ?? 0).ToString()}
|
||||
["org"] = org,
|
||||
["repo"] = repo,
|
||||
["issueNumber"] = issueNumber.ToString(),
|
||||
["parentNumber"] = (parentNumber ?? 0).ToString()
|
||||
};
|
||||
|
||||
await stream.OnNextAsync(new Event
|
||||
{
|
||||
Namespace = subject,
|
||||
Type = eventType,
|
||||
Subject = subject,
|
||||
Data = data
|
||||
|
@ -171,6 +172,7 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor
|
|||
};
|
||||
await stream.OnNextAsync(new Event
|
||||
{
|
||||
Namespace = subject,
|
||||
Type = eventType,
|
||||
Subject = subject,
|
||||
Data = data
|
||||
|
|
|
@ -60,13 +60,15 @@ public class CommunityManager : AiAgent<CommunityManagerState>
|
|||
|
||||
private async Task SendDesignedCreatedEvent(string socialMediaPost, string userId)
|
||||
{
|
||||
await PublishEvent(Consts.OrleansNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(EventTypes.SocialMediaPostCreated),
|
||||
Data = new Dictionary<string, string> {
|
||||
{ "UserId", userId },
|
||||
{ nameof(socialMediaPost), socialMediaPost}
|
||||
}
|
||||
Data = new Dictionary<string, string>
|
||||
{
|
||||
["UserId"] = userId,
|
||||
[nameof(socialMediaPost)] = socialMediaPost,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -74,4 +76,4 @@ public class CommunityManager : AiAgent<CommunityManagerState>
|
|||
{
|
||||
return Task.FromResult(_state.State.Data.WrittenSocialMediaPost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,8 +61,9 @@ public class GraphicDesigner : AiAgent<GraphicDesignerState>
|
|||
|
||||
private async Task SendDesignedCreatedEvent(string imageUri, string userId)
|
||||
{
|
||||
await PublishEvent(Consts.OrleansNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(EventTypes.GraphicDesignCreated),
|
||||
Data = new Dictionary<string, string> {
|
||||
{ "UserId", userId },
|
||||
|
|
|
@ -57,8 +57,9 @@ public class Writer : AiAgent<WriterState>, IWriter
|
|||
|
||||
private async Task SendDesignedCreatedEvent(string article, string userId)
|
||||
{
|
||||
await PublishEvent(Consts.OrleansNamespace, this.GetPrimaryKeyString(), new Event
|
||||
await PublishEvent(new Event
|
||||
{
|
||||
Namespace = this.GetPrimaryKeyString(),
|
||||
Type = nameof(EventTypes.ArticleCreated),
|
||||
Data = new Dictionary<string, string> {
|
||||
{ "UserId", userId },
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using Marketing.Agents;
|
||||
using Marketing.Agents;
|
||||
using Marketing.Events;
|
||||
using Marketing.Options;
|
||||
using Microsoft.AI.Agents.Abstractions;
|
||||
|
@ -53,6 +53,7 @@ public class Articles : ControllerBase
|
|||
|
||||
await stream.OnNextAsync(new Event
|
||||
{
|
||||
Namespace = UserId,
|
||||
Type = nameof(EventTypes.UserChatInput),
|
||||
Data = data
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
namespace Marketing.Options;
|
||||
namespace Marketing.Options;
|
||||
|
||||
public static class Consts
|
||||
{
|
||||
public const string OrleansNamespace = "DevPersonas";
|
||||
public const string OrleansNamespace = "default";
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
namespace Marketing.SignalRHub;
|
||||
namespace Marketing.SignalRHub;
|
||||
|
||||
using Microsoft.AI.Agents.Abstractions;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
@ -41,6 +41,7 @@ public class ArticleHub : Hub<IArticleHub>
|
|||
|
||||
await stream.OnNextAsync(new Event
|
||||
{
|
||||
Namespace = frontEndMessage.UserId,
|
||||
Type = nameof(EventTypes.UserChatInput),
|
||||
Data = data
|
||||
});
|
||||
|
@ -69,8 +70,9 @@ public class ArticleHub : Hub<IArticleHub>
|
|||
};
|
||||
await stream.OnNextAsync(new Event
|
||||
{
|
||||
Namespace = frontEndMessage.UserId,
|
||||
Type = nameof(EventTypes.UserConnected),
|
||||
Data = data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
qdrant
|
||||
orleans
|
||||
openai
|
|
@ -1,4 +1,4 @@
|
|||
using Dapr.Actors.Runtime;
|
||||
using Dapr.Actors.Runtime;
|
||||
using Dapr.Client;
|
||||
using Microsoft.AI.Agents.Abstractions;
|
||||
|
||||
|
@ -14,14 +14,15 @@ public abstract class Agent : Actor, IAgent
|
|||
}
|
||||
public abstract Task HandleEvent(Event item);
|
||||
|
||||
public async Task PublishEvent(string ns, string id, Event item)
|
||||
public async Task PublishEvent(Event item)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>() {
|
||||
{ "cloudevent.Type", item.Type },
|
||||
{ "cloudevent.Subject", item.Subject },
|
||||
{ "cloudevent.id", Guid.NewGuid().ToString()}
|
||||
};
|
||||
var metadata = new Dictionary<string, string>()
|
||||
{
|
||||
["cloudevent.Type"] = item.Type,
|
||||
["cloudevent.Subject"] = item.Subject,
|
||||
["cloudevent.id"] = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
await _daprClient.PublishEventAsync(ns, id, item, metadata).ConfigureAwait(false);
|
||||
await _daprClient.PublishEventAsync("default", item.Namespace, item, metadata).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.AI.Agents.Abstractions;
|
||||
using Microsoft.AI.Agents.Abstractions;
|
||||
using Orleans.Runtime;
|
||||
using Orleans.Streams;
|
||||
|
||||
|
@ -14,10 +14,10 @@ public abstract class Agent : Grain, IGrainWithStringKey, IAgent
|
|||
await HandleEvent(item).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public async Task PublishEvent(string ns, string id, Event item)
|
||||
public async Task PublishEvent(Event item)
|
||||
{
|
||||
var streamProvider = this.GetStreamProvider("StreamProvider");
|
||||
var streamId = StreamId.Create(ns, id);
|
||||
var streamId = StreamId.Create(ns: "default", key: item.Namespace);
|
||||
var stream = streamProvider.GetStream<Event>(streamId);
|
||||
await stream.OnNextAsync(item).ConfigureAwait(true);
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@ namespace Microsoft.AI.Agents.Orleans;
|
|||
internal struct EventSurrogate
|
||||
{
|
||||
[Id(0)]
|
||||
public Dictionary<string, string> Data { get; set; }
|
||||
public string Namespace { get; set; }
|
||||
[Id(1)]
|
||||
public string Type { get; set; }
|
||||
public Dictionary<string, string> Data { get; set; }
|
||||
[Id(2)]
|
||||
public string Type { get; set; }
|
||||
[Id(3)]
|
||||
public string Subject { get; set; }
|
||||
}
|
||||
|
||||
|
@ -19,12 +21,13 @@ internal sealed class EventSurrogateConverter :
|
|||
{
|
||||
public Event ConvertFromSurrogate(
|
||||
in EventSurrogate surrogate) =>
|
||||
new Event { Data = surrogate.Data, Subject = surrogate.Subject, Type = surrogate.Type };
|
||||
new() { Namespace = surrogate.Namespace, Data = surrogate.Data, Subject = surrogate.Subject, Type = surrogate.Type };
|
||||
|
||||
public EventSurrogate ConvertToSurrogate(
|
||||
in Event value) =>
|
||||
new EventSurrogate
|
||||
new()
|
||||
{
|
||||
Namespace = value.Namespace,
|
||||
Data = value.Data,
|
||||
Type = value.Type,
|
||||
Subject = value.Subject
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
using Microsoft.AI.Agents.Abstractions;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker.Client;
|
||||
|
||||
public abstract class Agent(IAgentContext context) : AgentBase(context), IAgent
|
||||
{
|
||||
Task IAgent.HandleEvent(Event item) => base.HandleEvent(item);
|
||||
|
||||
async Task IAgent.PublishEvent(Event item)
|
||||
{
|
||||
await base.PublishEvent(item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
using Agents;
|
||||
using Event = Microsoft.AI.Agents.Abstractions.Event;
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker.Client;
|
||||
|
||||
public abstract class AgentBase
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly Dictionary<string, TaskCompletionSource<RpcResponse>> _pendingRequests = [];
|
||||
private readonly Channel<object> _mailbox = Channel.CreateUnbounded<object>();
|
||||
private readonly IAgentContext _context;
|
||||
|
||||
protected internal AgentId AgentId => _context.AgentId;
|
||||
protected internal ILogger Logger => _context.Logger;
|
||||
protected internal IAgentContext Context => _context;
|
||||
|
||||
protected AgentBase(IAgentContext context)
|
||||
{
|
||||
_context = context;
|
||||
context.AgentInstance = this;
|
||||
Completion = Start();
|
||||
}
|
||||
|
||||
internal Task Completion { get; }
|
||||
|
||||
internal Task Start()
|
||||
{
|
||||
var didSuppress = false;
|
||||
if (!ExecutionContext.IsFlowSuppressed())
|
||||
{
|
||||
didSuppress = true;
|
||||
ExecutionContext.SuppressFlow();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Task.Run(RunMessagePump);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (didSuppress)
|
||||
{
|
||||
ExecutionContext.RestoreFlow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void ReceiveMessage(Message message) => _mailbox.Writer.TryWrite(message);
|
||||
|
||||
private async Task RunMessagePump()
|
||||
{
|
||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
||||
await foreach (var message in _mailbox.Reader.ReadAllAsync())
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case Message msg:
|
||||
await HandleRpcMessage(msg).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected message '{message}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Logger.LogError(ex, "Error processing message.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRpcMessage(Message msg)
|
||||
{
|
||||
switch (msg.MessageCase)
|
||||
{
|
||||
case Message.MessageOneofCase.Event:
|
||||
await HandleEvent(msg.Event.ToEvent()).ConfigureAwait(false);
|
||||
break;
|
||||
case Message.MessageOneofCase.Request:
|
||||
await OnRequestCore(msg.Request).ConfigureAwait(false);
|
||||
break;
|
||||
case Message.MessageOneofCase.Response:
|
||||
OnResponseCore(msg.Response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResponseCore(RpcResponse response)
|
||||
{
|
||||
var requestId = response.RequestId;
|
||||
TaskCompletionSource<RpcResponse>? completion;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_pendingRequests.Remove(requestId, out completion))
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown request id '{requestId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
completion.SetResult(response);
|
||||
}
|
||||
|
||||
private async ValueTask OnRequestCore(RpcRequest request)
|
||||
{
|
||||
RpcResponse response;
|
||||
|
||||
try
|
||||
{
|
||||
response = await HandleRequest(request).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
response = new RpcResponse { Error = ex.Message };
|
||||
}
|
||||
|
||||
await _context.SendResponseAsync(request, response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<RpcResponse> RequestAsync(AgentId target, string method, Dictionary<string, string> parameters)
|
||||
{
|
||||
var requestId = Guid.NewGuid().ToString();
|
||||
var request = new RpcRequest
|
||||
{
|
||||
Target = target,
|
||||
RequestId = requestId,
|
||||
Method = method,
|
||||
Data = JsonSerializer.Serialize(parameters)
|
||||
};
|
||||
|
||||
var completion = new TaskCompletionSource<RpcResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
lock (_lock)
|
||||
{
|
||||
_pendingRequests[requestId] = completion;
|
||||
}
|
||||
|
||||
await _context.SendRequestAsync(this, request).ConfigureAwait(false);
|
||||
return await completion.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected internal async ValueTask PublishEvent(Event @event)
|
||||
{
|
||||
var rpcEvent = @event.ToRpcEvent();
|
||||
await _context.PublishEventAsync(rpcEvent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected internal virtual Task<RpcResponse> HandleRequest(RpcRequest request) => Task.FromResult(new RpcResponse { Error = "Not implemented" });
|
||||
|
||||
protected internal virtual Task HandleEvent(Event @event) => Task.CompletedTask;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using Agents;
|
||||
using RpcEvent = Agents.Event;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker.Client;
|
||||
|
||||
internal sealed class AgentContext(AgentId agentId, AgentWorkerRuntime runtime, ILogger<AgentBase> logger) : IAgentContext
|
||||
{
|
||||
private readonly AgentWorkerRuntime _runtime = runtime;
|
||||
|
||||
public AgentId AgentId { get; } = agentId;
|
||||
public ILogger Logger { get; } = logger;
|
||||
public AgentBase? AgentInstance { get; set; }
|
||||
|
||||
public async ValueTask SendResponseAsync(RpcRequest request, RpcResponse response)
|
||||
{
|
||||
response.RequestId = request.RequestId;
|
||||
await _runtime.SendResponse(response);
|
||||
}
|
||||
|
||||
public async ValueTask SendRequestAsync(AgentBase agent, RpcRequest request)
|
||||
{
|
||||
await _runtime.SendRequest(agent, request);
|
||||
}
|
||||
|
||||
public async ValueTask PublishEventAsync(RpcEvent @event)
|
||||
{
|
||||
await _runtime.PublishEvent(@event);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using RpcAgentId = Agents.AgentId;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker.Client;
|
||||
|
||||
public sealed record class AgentId(string Name, string Namespace)
|
||||
{
|
||||
public static implicit operator RpcAgentId(AgentId agentId) => new()
|
||||
{
|
||||
Name = agentId.Name,
|
||||
Namespace = agentId.Namespace
|
||||
};
|
||||
|
||||
public static implicit operator AgentId(RpcAgentId agentId) => new(agentId.Name, agentId.Namespace);
|
||||
public override string ToString() => $"{Name}/{Namespace}";
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
using Agents;
|
||||
using Grpc.Core;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Collections.Concurrent;
|
||||
using RpcEvent = Agents.Event;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker.Client;
|
||||
|
||||
public static class HostBuilderExtensions
|
||||
{
|
||||
public static AgentApplicationBuilder AddAgentWorker(this IHostApplicationBuilder builder, string agentServiceAddress)
|
||||
{
|
||||
builder.Services.AddGrpcClient<AgentRpc.AgentRpcClient>(options => options.Address = new Uri(agentServiceAddress));
|
||||
builder.Services.AddSingleton<AgentWorkerRuntime>();
|
||||
builder.Services.AddSingleton<IHostedService>(sp => sp.GetRequiredService<AgentWorkerRuntime>());
|
||||
return new AgentApplicationBuilder(builder);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AgentApplicationBuilder(IHostApplicationBuilder builder)
|
||||
{
|
||||
public AgentApplicationBuilder AddAgent<TAgent>(string typeName) where TAgent : AgentBase
|
||||
{
|
||||
builder.Services.AddKeyedSingleton("AgentTypes", (sp, key) => Tuple.Create(typeName, typeof(TAgent)));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AgentWorkerRuntime(
|
||||
AgentRpc.AgentRpcClient client,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
IServiceProvider serviceProvider,
|
||||
[FromKeyedServices("AgentTypes")] IEnumerable<Tuple<string, Type>> agentTypes,
|
||||
ILogger<AgentWorkerRuntime> logger) : IHostedService, IDisposable
|
||||
{
|
||||
private readonly object _channelLock = new();
|
||||
private readonly ConcurrentDictionary<string, Type> _agentTypes = new();
|
||||
private readonly ConcurrentDictionary<(string Type, string Key), AgentBase> _agents = new();
|
||||
private readonly ConcurrentDictionary<string, (AgentBase Agent, string OriginalRequestId)> _pendingRequests = new();
|
||||
|
||||
private AsyncDuplexStreamingCall<Message, Message>? _channel;
|
||||
|
||||
private Task? _runTask;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_channel?.Dispose();
|
||||
}
|
||||
|
||||
private async Task RunMessagePump()
|
||||
{
|
||||
while (!hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||
{
|
||||
var channel = GetChannel();
|
||||
try
|
||||
{
|
||||
await foreach (var message in channel.ResponseStream.ReadAllAsync(hostApplicationLifetime.ApplicationStopping))
|
||||
{
|
||||
switch (message.MessageCase)
|
||||
{
|
||||
case Message.MessageOneofCase.Request:
|
||||
GetOrActivateAgent(message.Request.Target).ReceiveMessage(message);
|
||||
break;
|
||||
case Message.MessageOneofCase.Response:
|
||||
if (!_pendingRequests.TryRemove(message.Response.RequestId, out var request))
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected response '{message.Response}'");
|
||||
}
|
||||
|
||||
message.Response.RequestId = request.OriginalRequestId;
|
||||
request.Agent.ReceiveMessage(message);
|
||||
break;
|
||||
case Message.MessageOneofCase.Event:
|
||||
foreach (var agent in _agents.Values)
|
||||
{
|
||||
agent.ReceiveMessage(message);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected message '{message}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error reading from channel.");
|
||||
RecreateChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AgentBase GetOrActivateAgent(AgentId agentId)
|
||||
{
|
||||
if (!_agents.TryGetValue((agentId.Name, agentId.Namespace), out var agent))
|
||||
{
|
||||
if (_agentTypes.TryGetValue(agentId.Name, out var agentType))
|
||||
{
|
||||
var context = new AgentContext(agentId, this, serviceProvider.GetRequiredService<ILogger<AgentBase>>());
|
||||
agent = (AgentBase)ActivatorUtilities.CreateInstance(serviceProvider, agentType, context);
|
||||
_agents.TryAdd((agentId.Name, agentId.Namespace), agent);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Agent type '{agentId.Name}' is unknown.");
|
||||
}
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private async ValueTask RegisterAgentType(string type, Type agentType)
|
||||
{
|
||||
if (_agentTypes.TryAdd(type, agentType))
|
||||
{
|
||||
await WriteChannelAsync(new Message
|
||||
{
|
||||
RegisterAgentType = new RegisterAgentType
|
||||
{
|
||||
Type = type,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask SendResponse(RpcResponse response)
|
||||
{
|
||||
await WriteChannelAsync(new Message { Response = response });
|
||||
}
|
||||
|
||||
public async ValueTask SendRequest(AgentBase agent, RpcRequest request)
|
||||
{
|
||||
var requestId = Guid.NewGuid().ToString();
|
||||
_pendingRequests[requestId] = (agent, request.RequestId);
|
||||
request.RequestId = requestId;
|
||||
await WriteChannelAsync(new Message { Request = request });
|
||||
}
|
||||
|
||||
public async ValueTask PublishEvent(RpcEvent @event)
|
||||
{
|
||||
await WriteChannelAsync(new Message { Event = @event });
|
||||
}
|
||||
|
||||
private async Task WriteChannelAsync(Message message)
|
||||
{
|
||||
var channel = GetChannel();
|
||||
try
|
||||
{
|
||||
await channel.RequestStream.WriteAsync(message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Exception writing to channel.");
|
||||
RecreateChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private AsyncDuplexStreamingCall<Message, Message> GetChannel()
|
||||
{
|
||||
if (_channel is { } channel)
|
||||
{
|
||||
return channel;
|
||||
}
|
||||
|
||||
lock (_channelLock)
|
||||
{
|
||||
if (_channel is not null)
|
||||
{
|
||||
return _channel;
|
||||
}
|
||||
|
||||
return RecreateChannel(null);
|
||||
}
|
||||
}
|
||||
|
||||
private AsyncDuplexStreamingCall<Message, Message> RecreateChannel(AsyncDuplexStreamingCall<Message, Message>? channel)
|
||||
{
|
||||
if (_channel is null || _channel == channel)
|
||||
{
|
||||
lock (_channelLock)
|
||||
{
|
||||
if (_channel is null || _channel == channel)
|
||||
{
|
||||
_channel?.Dispose();
|
||||
_channel = client.OpenChannel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _channel;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_channel = GetChannel();
|
||||
_runTask = Start();
|
||||
|
||||
var tasks = new List<Task>(_agentTypes.Count);
|
||||
foreach (var (typeName, type) in agentTypes)
|
||||
{
|
||||
tasks.Add(RegisterAgentType(typeName, type).AsTask());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
internal Task Start()
|
||||
{
|
||||
var didSuppress = false;
|
||||
if (!ExecutionContext.IsFlowSuppressed())
|
||||
{
|
||||
didSuppress = true;
|
||||
ExecutionContext.SuppressFlow();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Task.Run(RunMessagePump);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (didSuppress)
|
||||
{
|
||||
ExecutionContext.RestoreFlow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
lock (_channelLock)
|
||||
{
|
||||
_channel?.Dispose();
|
||||
}
|
||||
|
||||
if (_runTask is { } task)
|
||||
{
|
||||
await task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using Agents;
|
||||
using RpcEvent = Agents.Event;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker.Client;
|
||||
|
||||
public interface IAgentContext
|
||||
{
|
||||
AgentId AgentId { get; }
|
||||
AgentBase? AgentInstance { get; set; }
|
||||
ILogger Logger { get; }
|
||||
ValueTask SendResponseAsync(RpcRequest request, RpcResponse response);
|
||||
ValueTask SendRequestAsync(AgentBase agent, RpcRequest request);
|
||||
ValueTask PublishEventAsync(RpcEvent @event);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="..\..\..\protos\agent_worker.proto" GrpcServices="Client" Link="Protos\agent_worker.proto" />
|
||||
<Compile Include="..\Shared\RpcEventExtensions.cs" Link="RpcEventExtensions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AI.Agents\Microsoft.AI.Agents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" />
|
||||
<PackageReference Include="Grpc.Net.ClientFactory" />
|
||||
<PackageReference Include="Grpc.Tools">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,22 @@
|
|||
using Orleans.Runtime;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
internal sealed class AgentStateGrain([PersistentState("state", "agent-state")] IPersistentState<Dictionary<string, object>> state) : Grain, IAgentStateGrain
|
||||
{
|
||||
public ValueTask<(Dictionary<string, object> State, string ETag)> ReadStateAsync()
|
||||
{
|
||||
return new((state.State, state.Etag));
|
||||
}
|
||||
|
||||
public async ValueTask<string> WriteStateAsync(Dictionary<string, object> value, string eTag)
|
||||
{
|
||||
if (string.Equals(state.Etag, eTag, StringComparison.Ordinal))
|
||||
{
|
||||
state.State = value;
|
||||
await state.WriteStateAsync();
|
||||
}
|
||||
|
||||
return state.Etag;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Orleans.Serialization;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
public static class AgentWorkerHostingExtensions
|
||||
{
|
||||
public static IHostApplicationBuilder AddAgentService(this IHostApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddGrpc();
|
||||
builder.Services.AddSerializer(serializer => serializer.AddProtobufSerializer());
|
||||
|
||||
// Ensure Orleans is added before the hosted service to guarantee that it starts first.
|
||||
builder.UseOrleans();
|
||||
builder.Services.AddSingleton<WorkerGateway>();
|
||||
builder.Services.AddSingleton<IHostedService>(sp => sp.GetRequiredService<WorkerGateway>());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplication MapAgentService(this WebApplication app)
|
||||
{
|
||||
app.MapGrpcService<WorkerGatewayService>();
|
||||
return app;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
using Agents;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
public sealed class AgentWorkerRegistryGrain : Grain, IAgentWorkerRegistryGrain
|
||||
{
|
||||
// TODO: use persistent state for some of these or (better) extend Orleans to implement some of this natively.
|
||||
private readonly Dictionary<IWorkerGateway, WorkerState> _workerStates = [];
|
||||
private readonly Dictionary<string, List<IWorkerGateway>> _supportedAgentTypes = [];
|
||||
private readonly Dictionary<(string Type, string Key), IWorkerGateway> _agentDirectory = [];
|
||||
private readonly TimeSpan _agentTimeout = TimeSpan.FromMinutes(1);
|
||||
|
||||
public override Task OnActivateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
RegisterTimer(static state => ((AgentWorkerRegistryGrain)state)!.PurgeInactiveWorkers(), this, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
|
||||
return base.OnActivateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private Task PurgeInactiveWorkers()
|
||||
{
|
||||
foreach (var (worker, state) in _workerStates)
|
||||
{
|
||||
if (DateTimeOffset.UtcNow - state.LastSeen > _agentTimeout)
|
||||
{
|
||||
_workerStates.Remove(worker);
|
||||
foreach (var type in state.SupportedTypes)
|
||||
{
|
||||
if (_supportedAgentTypes.TryGetValue(type, out var workers))
|
||||
{
|
||||
workers.Remove(worker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask AddWorker(IWorkerGateway worker)
|
||||
{
|
||||
GetOrAddWorker(worker);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private WorkerState GetOrAddWorker(IWorkerGateway worker)
|
||||
{
|
||||
if (!_workerStates.TryGetValue(worker, out var workerState))
|
||||
{
|
||||
workerState = _workerStates[worker] = new();
|
||||
}
|
||||
|
||||
workerState.LastSeen = DateTimeOffset.UtcNow;
|
||||
return workerState;
|
||||
}
|
||||
|
||||
public ValueTask RegisterAgentType(string type, IWorkerGateway worker)
|
||||
{
|
||||
if (!_supportedAgentTypes.TryGetValue(type, out var supportedAgentTypes))
|
||||
{
|
||||
supportedAgentTypes = _supportedAgentTypes[type] = [];
|
||||
}
|
||||
|
||||
if (!supportedAgentTypes.Contains(worker))
|
||||
{
|
||||
supportedAgentTypes.Add(worker);
|
||||
}
|
||||
|
||||
var workerState = GetOrAddWorker(worker);
|
||||
workerState.SupportedTypes.Add(type);
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask RemoveWorker(IWorkerGateway worker)
|
||||
{
|
||||
if (_workerStates.Remove(worker, out var state))
|
||||
{
|
||||
foreach (var type in state.SupportedTypes)
|
||||
{
|
||||
if (_supportedAgentTypes.TryGetValue(type, out var workers))
|
||||
{
|
||||
workers.Remove(worker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask UnregisterAgentType(string type, IWorkerGateway worker)
|
||||
{
|
||||
if (_workerStates.TryGetValue(worker, out var state))
|
||||
{
|
||||
state.SupportedTypes.Remove(type);
|
||||
}
|
||||
|
||||
if (_supportedAgentTypes.TryGetValue(type, out var workers))
|
||||
{
|
||||
workers.Remove(worker);
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<IWorkerGateway?> GetCompatibleWorker(string type) => new(GetCompatibleWorkerCore(type));
|
||||
|
||||
private IWorkerGateway? GetCompatibleWorkerCore(string type)
|
||||
{
|
||||
if (_supportedAgentTypes.TryGetValue(type, out var workers))
|
||||
{
|
||||
// Return a random compatible worker.
|
||||
return workers[Random.Shared.Next(workers.Count)];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ValueTask<(IWorkerGateway? Gateway, bool NewPlacment)> GetOrPlaceAgent(AgentId agentId)
|
||||
{
|
||||
bool isNewPlacement;
|
||||
if (!_agentDirectory.TryGetValue((agentId.Name, agentId.Namespace), out var worker) || !_workerStates.ContainsKey(worker))
|
||||
{
|
||||
worker = GetCompatibleWorkerCore(agentId.Name);
|
||||
if (worker is not null)
|
||||
{
|
||||
// New activation.
|
||||
_agentDirectory[(agentId.Name, agentId.Namespace)] = worker;
|
||||
isNewPlacement = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No activation, and failed to place.
|
||||
isNewPlacement = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Existing activation.
|
||||
isNewPlacement = false;
|
||||
}
|
||||
|
||||
return new((worker, isNewPlacement));
|
||||
}
|
||||
|
||||
private sealed class WorkerState
|
||||
{
|
||||
public HashSet<string> SupportedTypes { get; set; } = [];
|
||||
public DateTimeOffset LastSeen { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
internal interface IAgentStateGrain : IGrainWithStringKey
|
||||
{
|
||||
ValueTask<(Dictionary<string, object> State, string ETag)> ReadStateAsync();
|
||||
ValueTask<string> WriteStateAsync(Dictionary<string, object> state, string eTag);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using Agents;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
public interface IAgentWorkerRegistryGrain : IGrainWithIntegerKey
|
||||
{
|
||||
ValueTask RegisterAgentType(string type, IWorkerGateway worker);
|
||||
ValueTask UnregisterAgentType(string type, IWorkerGateway worker);
|
||||
ValueTask AddWorker(IWorkerGateway worker);
|
||||
ValueTask RemoveWorker(IWorkerGateway worker);
|
||||
ValueTask<(IWorkerGateway? Gateway, bool NewPlacment)> GetOrPlaceAgent(AgentId agentId);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using Agents;
|
||||
using RpcEvent = Agents.Event;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
public interface IWorkerGateway : IGrainObserver
|
||||
{
|
||||
ValueTask<RpcResponse> InvokeRequest(RpcRequest request);
|
||||
ValueTask BroadcastEvent(RpcEvent evt);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="..\..\..\protos\agent_worker.proto" GrpcServices="Server" Link="Protos\agent_worker.proto" />
|
||||
<Compile Include="..\Shared\RpcEventExtensions.cs" Link="RpcEventExtensions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AI.Agents\Microsoft.AI.Agents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Orleans.Serialization.Protobuf" />
|
||||
<PackageReference Include="Microsoft.Orleans.Server" />
|
||||
<PackageReference Include="Microsoft.Orleans.Streaming" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,273 @@
|
|||
using Grpc.Core;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Orleans.Runtime;
|
||||
using Agents;
|
||||
using Orleans.Streams;
|
||||
using Event = Microsoft.AI.Agents.Abstractions.Event;
|
||||
using RpcEvent = Agents.Event;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
internal sealed class WorkerGateway : BackgroundService, IWorkerGateway
|
||||
{
|
||||
private static readonly TimeSpan s_agentResponseTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
private readonly ILogger<WorkerGateway> _logger;
|
||||
private readonly IClusterClient _clusterClient;
|
||||
private readonly IAgentWorkerRegistryGrain _gatewayRegistry;
|
||||
private readonly IWorkerGateway _reference;
|
||||
|
||||
// The local mapping of agents to worker processes.
|
||||
private readonly ConcurrentDictionary<WorkerProcessConnection, WorkerProcessConnection> _workers = new();
|
||||
|
||||
// The agents supported by each worker process.
|
||||
private readonly ConcurrentDictionary<string, List<WorkerProcessConnection>> _supportedAgentTypes = [];
|
||||
|
||||
// The mapping from agent id to worker process.
|
||||
private readonly ConcurrentDictionary<(string Type, string Key), WorkerProcessConnection> _agentDirectory = new();
|
||||
|
||||
// RPC
|
||||
private readonly ConcurrentDictionary<(WorkerProcessConnection, string), TaskCompletionSource<RpcResponse>> _pendingRequests = new();
|
||||
|
||||
public WorkerGateway(IClusterClient clusterClient, ILogger<WorkerGateway> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_clusterClient = clusterClient;
|
||||
_reference = clusterClient.CreateObjectReference<IWorkerGateway>(this);
|
||||
_gatewayRegistry = clusterClient.GetGrain<IAgentWorkerRegistryGrain>(0);
|
||||
}
|
||||
|
||||
public async ValueTask BroadcastEvent(RpcEvent evt)
|
||||
{
|
||||
var tasks = new List<Task>(_agentDirectory.Count);
|
||||
foreach (var (_, connection) in _agentDirectory)
|
||||
{
|
||||
tasks.Add(connection.SendMessage(new Message { Event = evt }));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async ValueTask<RpcResponse> InvokeRequest(RpcRequest request)
|
||||
{
|
||||
(string Type, string Key) agentId = (request.Target.Name, request.Target.Namespace);
|
||||
if (!_agentDirectory.TryGetValue(agentId, out var connection) || connection.Completion.IsCompleted)
|
||||
{
|
||||
// Activate the agent on a compatible worker process.
|
||||
if (_supportedAgentTypes.TryGetValue(request.Target.Name, 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 });
|
||||
|
||||
// Wait for the response and send it back to the caller.
|
||||
var response = await completion.Task.WaitAsync(s_agentResponseTimeout);
|
||||
response.RequestId = originalRequestId;
|
||||
return response;
|
||||
}
|
||||
|
||||
void DispatchResponse(WorkerProcessConnection connection, RpcResponse response)
|
||||
{
|
||||
if (!_pendingRequests.TryRemove((connection, response.RequestId), out var completion))
|
||||
{
|
||||
_logger.LogWarning("Received response for unknown request.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete the request.
|
||||
completion.SetResult(response);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _gatewayRegistry.AddWorker(_reference);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Error adding worker to registry.");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _gatewayRegistry.RemoveWorker(_reference);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Error removing worker from registry.");
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task OnReceivedMessageAsync(WorkerProcessConnection connection, Message message)
|
||||
{
|
||||
_logger.LogInformation("Received message {Message} from connection {Connection}.", message, connection);
|
||||
switch (message.MessageCase)
|
||||
{
|
||||
case Message.MessageOneofCase.Request:
|
||||
await DispatchRequestAsync(connection, message.Request);
|
||||
break;
|
||||
case Message.MessageOneofCase.Response:
|
||||
DispatchResponse(connection, message.Response);
|
||||
break;
|
||||
case Message.MessageOneofCase.Event:
|
||||
await DispatchEventAsync(message.Event);
|
||||
break;
|
||||
case Message.MessageOneofCase.RegisterAgentType:
|
||||
await RegisterAgentTypeAsync(connection, message.RegisterAgentType);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown message type for message '{message}'.");
|
||||
};
|
||||
}
|
||||
|
||||
async ValueTask RegisterAgentTypeAsync(WorkerProcessConnection connection, RegisterAgentType msg)
|
||||
{
|
||||
connection.AddSupportedType(msg.Type);
|
||||
_supportedAgentTypes.GetOrAdd(msg.Type, _ => []).Add(connection);
|
||||
|
||||
await _clusterClient.GetGrain<IAgentWorkerRegistryGrain>(0).RegisterAgentType(msg.Type, _reference);
|
||||
}
|
||||
|
||||
async ValueTask DispatchEventAsync(RpcEvent evt)
|
||||
{
|
||||
var topic = _clusterClient.GetStreamProvider("agents").GetStream<Event>(StreamId.Create(evt.Namespace, evt.Type));
|
||||
await topic.OnNextAsync(evt.ToEvent());
|
||||
}
|
||||
|
||||
async ValueTask DispatchRequestAsync(WorkerProcessConnection connection, RpcRequest request)
|
||||
{
|
||||
var requestId = request.RequestId;
|
||||
if (request.Target is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Request message is missing a target. Message: '{request}'.");
|
||||
}
|
||||
|
||||
if (string.Equals("runtime", request.Target.Name, StringComparison.Ordinal))
|
||||
{
|
||||
if (string.Equals("subscribe", request.Method))
|
||||
{
|
||||
await InvokeRequestDelegate(connection, request, async (_) =>
|
||||
{
|
||||
await SubscribeToTopic(connection, request);
|
||||
return new RpcResponse { Result = "Ok" };
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await InvokeRequestDelegate(connection, request, async request =>
|
||||
{
|
||||
var (gateway, isPlacement) = await _gatewayRegistry.GetOrPlaceAgent(request.Target);
|
||||
if (gateway is null)
|
||||
{
|
||||
return new RpcResponse { Error = "Agent not found and no compatible gateways were found." };
|
||||
}
|
||||
|
||||
if (isPlacement)
|
||||
{
|
||||
// Activate the worker: load state
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Forward the message to the gateway and return the result.
|
||||
return await gateway.InvokeRequest(request);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task InvokeRequestDelegate(WorkerProcessConnection connection, RpcRequest request, Func<RpcRequest, Task<RpcResponse>> func)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await func(request);
|
||||
response.RequestId = request.RequestId;
|
||||
await connection.ResponseStream.WriteAsync(new Message { Response = response });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await connection.ResponseStream.WriteAsync(new Message { Response = new RpcResponse { RequestId = request.RequestId, Error = ex.Message } });
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask SubscribeToTopic(WorkerProcessConnection connection, RpcRequest request)
|
||||
{
|
||||
// Subscribe to a topic
|
||||
var parameters = JsonSerializer.Deserialize<Dictionary<string, string>>(request.Data)
|
||||
?? throw new ArgumentException($"Request data does not contain required payload format: {{\"namespace\": \"string\", \"type\": \"string\"}}.");
|
||||
var ns = parameters["namespace"];
|
||||
var type = parameters["type"];
|
||||
var topic = _clusterClient.GetStreamProvider("agents").GetStream<Event>(StreamId.Create(ns: type, key: ns));
|
||||
await topic.SubscribeAsync(OnNextAsync);
|
||||
return;
|
||||
|
||||
async Task OnNextAsync(IList<SequentialItem<Event>> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
var evt = item.Item.ToRpcEvent();
|
||||
evt.Namespace = ns;
|
||||
evt.Type = evt.Type;
|
||||
var payload = new Dictionary<string, string>
|
||||
{
|
||||
["sequenceId"] = item.Token.SequenceNumber.ToString(),
|
||||
["eventIdx"] = item.Token.EventIndex.ToString()
|
||||
};
|
||||
evt.Data = JsonSerializer.Serialize(payload);
|
||||
await connection.ResponseStream.WriteAsync(new Message { Event = evt });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal Task ConnectToWorkerProcess(IAsyncStreamReader<Message> requestStream, IServerStreamWriter<Message> responseStream, ServerCallContext context)
|
||||
{
|
||||
_logger.LogInformation("Received new connection from {Peer}.", context.Peer);
|
||||
var workerProcess = new WorkerProcessConnection(this, requestStream, responseStream, context);
|
||||
_workers[workerProcess] = workerProcess;
|
||||
return workerProcess.Completion;
|
||||
}
|
||||
|
||||
internal void OnRemoveWorkerProcess(WorkerProcessConnection workerProcess)
|
||||
{
|
||||
_workers.TryRemove(workerProcess, out _);
|
||||
var types = workerProcess.GetSupportedTypes();
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (_supportedAgentTypes.TryGetValue(type, out var supported))
|
||||
{
|
||||
supported.Remove(workerProcess);
|
||||
}
|
||||
}
|
||||
|
||||
// Any agents activated on that worker are also gone.
|
||||
foreach (var pair in _agentDirectory)
|
||||
{
|
||||
if (pair.Value == workerProcess)
|
||||
{
|
||||
((IDictionary<(string Type, string Key), WorkerProcessConnection>)_agentDirectory).Remove(pair);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using Grpc.Core;
|
||||
using Agents;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
// gRPC service which handles communication between the agent worker and the cluster.
|
||||
internal sealed class WorkerGatewayService(WorkerGateway agentWorker) : AgentRpc.AgentRpcBase
|
||||
{
|
||||
public override async Task OpenChannel(IAsyncStreamReader<Message> requestStream, IServerStreamWriter<Message> responseStream, ServerCallContext context)
|
||||
{
|
||||
await agentWorker.ConnectToWorkerProcess(requestStream, responseStream, context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
using Grpc.Core;
|
||||
using Agents;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
internal sealed class WorkerProcessConnection : IAsyncDisposable
|
||||
{
|
||||
private static long s_nextConnectionId;
|
||||
private readonly string _connectionId = Interlocked.Increment(ref s_nextConnectionId).ToString();
|
||||
private readonly object _lock = new();
|
||||
private readonly HashSet<string> _supportedTypes = [];
|
||||
private readonly WorkerGateway _gateway;
|
||||
private readonly CancellationTokenSource _shutdownCancellationToken = new();
|
||||
|
||||
public WorkerProcessConnection(WorkerGateway agentWorker, IAsyncStreamReader<Message> requestStream, IServerStreamWriter<Message> responseStream, ServerCallContext context)
|
||||
{
|
||||
_gateway = agentWorker;
|
||||
RequestStream = requestStream;
|
||||
ResponseStream = responseStream;
|
||||
ServerCallContext = context;
|
||||
Completion = Start();
|
||||
}
|
||||
|
||||
public IAsyncStreamReader<Message> RequestStream { get; }
|
||||
public IServerStreamWriter<Message> ResponseStream { get; }
|
||||
public ServerCallContext ServerCallContext { get; }
|
||||
|
||||
public void AddSupportedType(string type)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_supportedTypes.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
public HashSet<string> GetSupportedTypes()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new HashSet<string>(_supportedTypes);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendMessage(Message message)
|
||||
{
|
||||
await ResponseStream.WriteAsync(message);
|
||||
}
|
||||
|
||||
public Task Completion { get; }
|
||||
|
||||
private Task Start()
|
||||
{
|
||||
var didSuppress = false;
|
||||
if (!ExecutionContext.IsFlowSuppressed())
|
||||
{
|
||||
didSuppress = true;
|
||||
ExecutionContext.SuppressFlow();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Task.Run(Run);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (didSuppress)
|
||||
{
|
||||
ExecutionContext.RestoreFlow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
||||
try
|
||||
{
|
||||
await foreach (var message in RequestStream.ReadAllAsync(_shutdownCancellationToken.Token))
|
||||
{
|
||||
|
||||
// Fire and forget
|
||||
_gateway.OnReceivedMessageAsync(this, message).Ignore();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gateway.OnRemoveWorkerProcess(this);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_shutdownCancellationToken.Cancel();
|
||||
await Completion.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
}
|
||||
|
||||
public override string ToString() => $"Connection-{_connectionId}";
|
||||
}
|
|
@ -6,6 +6,7 @@ namespace Microsoft.AI.Agents.Abstractions;
|
|||
public class Event
|
||||
{
|
||||
public required Dictionary<string, string> Data { get; set; }
|
||||
public required string Namespace { get; set; }
|
||||
public required string Type { get; set; }
|
||||
public string Subject { get; set; } = "";
|
||||
}
|
||||
|
|
|
@ -3,5 +3,5 @@ namespace Microsoft.AI.Agents.Abstractions;
|
|||
public interface IAgent
|
||||
{
|
||||
Task HandleEvent(Event item);
|
||||
Task PublishEvent(string ns, string id, Event item);
|
||||
}
|
||||
Task PublishEvent(Event item);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
using System.Text.Json;
|
||||
using Event = Microsoft.AI.Agents.Abstractions.Event;
|
||||
using RpcEvent = Agents.Event;
|
||||
|
||||
namespace Microsoft.AI.Agents.Worker;
|
||||
|
||||
public static class RpcEventExtensions
|
||||
{
|
||||
public static RpcEvent ToRpcEvent(this Event input)
|
||||
{
|
||||
var result = new RpcEvent
|
||||
{
|
||||
Namespace = input.Namespace,
|
||||
Type = input.Type,
|
||||
};
|
||||
|
||||
if (input.Data is not null)
|
||||
{
|
||||
result.Data = JsonSerializer.Serialize(input.Data);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Event ToEvent(this RpcEvent input)
|
||||
{
|
||||
var result = new Event
|
||||
{
|
||||
Type = input.Type,
|
||||
Subject = input.Namespace,
|
||||
Namespace = input.Namespace,
|
||||
Data = []
|
||||
};
|
||||
|
||||
if (input.Data is not null)
|
||||
{
|
||||
result.Data = JsonSerializer.Deserialize<Dictionary<string, string>>(input.Data)!;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package agents;
|
||||
|
||||
message AgentId {
|
||||
string name = 1;
|
||||
string namespace = 2;
|
||||
}
|
||||
|
||||
message RpcRequest {
|
||||
string request_id = 1;
|
||||
AgentId source = 2;
|
||||
AgentId target = 3;
|
||||
string method = 4;
|
||||
string data = 5;
|
||||
map<string, string> metadata = 6;
|
||||
}
|
||||
|
||||
message RpcResponse {
|
||||
string request_id = 1;
|
||||
string result = 2;
|
||||
string error = 3;
|
||||
map<string, string> metadata = 4;
|
||||
}
|
||||
|
||||
message Event {
|
||||
string namespace = 1;
|
||||
string type = 2;
|
||||
string data = 3;
|
||||
map<string, string> metadata = 4;
|
||||
}
|
||||
|
||||
message RegisterAgentType {
|
||||
string type = 1;
|
||||
}
|
||||
|
||||
service AgentRpc {
|
||||
rpc OpenChannel (stream Message) returns (stream Message);
|
||||
}
|
||||
|
||||
message Message {
|
||||
oneof message {
|
||||
RpcRequest request = 1;
|
||||
RpcResponse response = 2;
|
||||
Event event = 3;
|
||||
RegisterAgentType registerAgentType = 4;
|
||||
}
|
||||
}
|
||||
|
|
@ -163,3 +163,6 @@ cython_debug/
|
|||
|
||||
/docs/src/reference
|
||||
.DS_Store
|
||||
|
||||
# Generated proto files
|
||||
src/agnext/worker/protos/agent*
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: Current File",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": false
|
||||
}
|
||||
]
|
||||
}
|
|
@ -28,6 +28,7 @@ apidoc_template_dir = '_apidoc_templates'
|
|||
apidoc_separate_modules = True
|
||||
apidoc_extra_args = ["--no-toc"]
|
||||
napoleon_custom_sections = [('Returns', 'params_style')]
|
||||
apidoc_excluded_paths = ["./worker/protos/"]
|
||||
|
||||
templates_path = []
|
||||
exclude_patterns = ["reference/agnext.rst"]
|
||||
|
|
|
@ -15,6 +15,10 @@ Generally, messages are one of:
|
|||
|
||||
Messages are purely data, and should not contain any logic.
|
||||
|
||||
```{tip}
|
||||
It is *strongly* recommended that messages are Pydantic models. This allows for easy serialization and deserialization of messages, and provides a clear schema for the message.
|
||||
```
|
||||
|
||||
<!-- ### Required Message Types
|
||||
|
||||
At the core framework level there is *no requirement* of which message types are handled by an agent. However, some behavior patterns require agents understand certain message types. For an agent to participate in these patterns, it must understand any such required message types.
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# Agent Worker Protocol
|
||||
|
||||
## System architecture
|
||||
|
||||
The system consists of multiple processes, each being either a _service_ process or a _worker_ process.
|
||||
Worker processes host application code (agents) and connect to a service process.
|
||||
Workers advertise the agents which they support to the service, so the service can decide which worker to place agents on.
|
||||
Service processes coordinate placement of agents on worker processes and facilitate communication between agents.
|
||||
|
||||
Agent instances are identified by the tuple of `(namespace: str, name: str)`.
|
||||
Both _namespace_ and _name_ are application-defined.
|
||||
The _namespace_ has no semantics implied by the system: it is free-form, and any semantics are implemented by application code.
|
||||
The _name_ is used to route requests to a worker which supports agents with that name.
|
||||
Workers advertise the set of agent names which they are capable of hosting to the service.
|
||||
Workers activate agents in response to messages received from the service.
|
||||
The service uses the _name_ to determine where to place currently-inactive agents, maintaining a mapping from agent name to a set of workers which support that agent.
|
||||
The service maintains a _directory_ mapping active agent ids to worker processes which host the identified agent.
|
||||
|
||||
### Agent lifecycle
|
||||
|
||||
Agents are never explicitly created or destroyed. When a request is received for an agent which is not currently active, it is the responsibility of the service to select a worker which is capable of hosting that agent, and to route the request to that worker.
|
||||
|
||||
## Worker protocol flow
|
||||
|
||||
The worker protocol has three phases, following the lifetime of the worker: initiation, operation, and termination.
|
||||
|
||||
### Initialization
|
||||
|
||||
When the worker process starts, it initiates a connection to a service process, establishing a bi-directional communication channel which messages are passed across.
|
||||
Next, the worker issues zero or more `RegisterAgentType(name: str)` messages, which tell the service the names of the agents which it is able to host.
|
||||
|
||||
* TODO: What other metadata should the worker give to the service?
|
||||
* TODO: Should we give the worker a unique id which can be used to identify it for its lifetime? Should we allow this to be specified by the worker process itself?
|
||||
|
||||
### Operation
|
||||
|
||||
Once the connection is established, and the service knows which agents the worker is capable of hosting, the worker may begin receiving requests for agents which it must host.
|
||||
Placement of agents happens in response to an `Event(...)` or `RpcRequest(...)` message.
|
||||
The worker maintains a _catalog_ of locally active agents: a mapping from agent id to agent instance.
|
||||
If a message arrives for an agent which does not have a corresponding entry in the catalog, the worker activates a new instance of that agent and inserts it into the catalog.
|
||||
The worker dispatches the message to the agent:
|
||||
|
||||
* For an `Event`, the agent processes the message and no response is generated.
|
||||
* For an `RpcRequest` message, the agent processes the message and generates a response of type `RpcResponse`. The worker routes the response to the original sender.
|
||||
|
||||
The worker maintains a mapping of outstanding requests, identified by `RpcRequest.id`, to a promise for a future `RpcResponse`.
|
||||
When an `RpcResponse` is received, the worker finds the corresponding request id and fulfils the promise using that response.
|
||||
If no response is received in a specified time frame (eg, 30s), the worker breaks the promise with a timeout error.
|
||||
|
||||
### Termination
|
||||
|
||||
When the worker is ready to shutdown, it closes the connection to the service and terminates. The service de-registers the worker and all agent instances which were hosted on it.
|
|
@ -43,6 +43,7 @@ that demonstrate how to use AGNext.
|
|||
core-concepts/cancellation
|
||||
core-concepts/logging
|
||||
core-concepts/namespace
|
||||
core-concepts/worker_protocol
|
||||
|
||||
.. toctree::
|
||||
:caption: Guides
|
||||
|
@ -61,6 +62,7 @@ that demonstrate how to use AGNext.
|
|||
reference/agnext.components
|
||||
reference/agnext.application
|
||||
reference/agnext.core
|
||||
reference/agnext.worker
|
||||
|
||||
.. toctree::
|
||||
:caption: Other
|
||||
|
|
|
@ -18,7 +18,10 @@ dependencies = [
|
|||
"pillow",
|
||||
"aiohttp",
|
||||
"typing-extensions",
|
||||
"pydantic>=1.10,<3"
|
||||
"pydantic>=1.10,<3",
|
||||
"types-aiofiles",
|
||||
"grpcio",
|
||||
"protobuf"
|
||||
]
|
||||
|
||||
[tool.hatch.envs.default]
|
||||
|
@ -41,7 +44,8 @@ dependencies = [
|
|||
"pip",
|
||||
"pytest",
|
||||
"pytest-xdist",
|
||||
"pytest-mock"
|
||||
"pytest-mock",
|
||||
"grpcio-tools",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.default.extra-scripts]
|
||||
|
@ -67,7 +71,11 @@ python = ["3.10", "3.11", "3.12"]
|
|||
|
||||
[tool.hatch.envs.docs]
|
||||
dependencies = [
|
||||
"sphinx", "furo", "sphinxcontrib-apidoc", "myst-parser", "sphinx-autobuild"
|
||||
"sphinx",
|
||||
"furo",
|
||||
"sphinxcontrib-apidoc",
|
||||
"myst-parser",
|
||||
"sphinx-autobuild",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.docs.scripts]
|
||||
|
@ -78,7 +86,7 @@ check = "sphinx-build --fail-on-warning docs/src docs/build"
|
|||
[tool.ruff]
|
||||
line-length = 120
|
||||
fix = true
|
||||
exclude = ["build", "dist"]
|
||||
exclude = ["build", "dist", "src/agnext/worker/protos"]
|
||||
target-version = "py310"
|
||||
include = ["src/**", "examples/*.py"]
|
||||
|
||||
|
@ -95,6 +103,7 @@ ignore = ["F401", "E501"]
|
|||
|
||||
[tool.mypy]
|
||||
files = ["src", "examples", "tests"]
|
||||
exclude = ["src/agnext/worker/protos"]
|
||||
|
||||
strict = true
|
||||
python_version = "3.10"
|
||||
|
@ -117,7 +126,23 @@ include = ["src", "tests", "examples"]
|
|||
typeCheckingMode = "strict"
|
||||
reportUnnecessaryIsInstance = false
|
||||
reportMissingTypeStubs = false
|
||||
exclude = ["src/agnext/worker/protos"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.hatch.build.hooks.protobuf]
|
||||
dependencies = ["hatch-protobuf", "mypy-protobuf~=3.0"]
|
||||
generate_pyi = false
|
||||
proto_paths = ["../protos"]
|
||||
output_path = "src/agnext/worker/protos"
|
||||
|
||||
|
||||
[[tool.hatch.build.hooks.protobuf.generators]]
|
||||
name = "mypy"
|
||||
outputs = ["{proto_path}/{proto_name}_pb2.pyi"]
|
||||
|
||||
[[tool.hatch.build.hooks.protobuf.generators]]
|
||||
name = "mypy_grpc"
|
||||
outputs = ["{proto_path}/{proto_name}_pb2_grpc.pyi"]
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
The :mod:`agnext.worker` module provides a set of classes for creating distributed agents
|
||||
"""
|
||||
|
||||
from .worker_runtime import WorkerAgentRuntime
|
||||
|
||||
__all__ = ["WorkerAgentRuntime"]
|
|
@ -0,0 +1,19 @@
|
|||
"""
|
||||
The :mod:`agnext.worker.protos` module provides Google Protobuf classes for agent-worker communication
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .agent_worker_pb2 import Event, Message, RegisterAgentType, RpcRequest, RpcResponse, AgentId
|
||||
from .agent_worker_pb2_grpc import AgentRpcStub
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .agent_worker_pb2_grpc import AgentRpcAsyncStub
|
||||
__all__ = ["RpcRequest", "RpcResponse", "Event", "RegisterAgentType", "AgentRpcAsyncStub", "AgentRpcStub", "Message", "AgentId"]
|
||||
else:
|
||||
__all__ = ["RpcRequest", "RpcResponse", "Event", "RegisterAgentType", "AgentRpcStub", "Message", "AgentId"]
|
|
@ -0,0 +1,397 @@
|
|||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from asyncio import Future, Task
|
||||
from collections import defaultdict
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncIterable,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
ClassVar,
|
||||
DefaultDict,
|
||||
Dict,
|
||||
List,
|
||||
Mapping,
|
||||
ParamSpec,
|
||||
Set,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
import grpc
|
||||
from grpc.aio import StreamStreamCall
|
||||
from typing_extensions import Self
|
||||
|
||||
from agnext.application.message_serialization import Serialization
|
||||
|
||||
from ..core import (
|
||||
Agent,
|
||||
AgentId,
|
||||
AgentMetadata,
|
||||
AgentProxy,
|
||||
AgentRuntime,
|
||||
CancellationToken,
|
||||
)
|
||||
from .protos import AgentId as AgentIdProto
|
||||
from .protos import (
|
||||
AgentRpcStub,
|
||||
Event,
|
||||
Message,
|
||||
RegisterAgentType,
|
||||
RpcRequest,
|
||||
RpcResponse,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .protos import AgentRpcAsyncStub
|
||||
|
||||
logger = logging.getLogger("agnext")
|
||||
event_logger = logging.getLogger("agnext.events")
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class PublishMessageEnvelope:
|
||||
"""A message envelope for publishing messages to all agents that can handle
|
||||
the message of the type T."""
|
||||
|
||||
message: Any
|
||||
cancellation_token: CancellationToken
|
||||
sender: AgentId | None
|
||||
namespace: str
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class SendMessageEnvelope:
|
||||
"""A message envelope for sending a message to a specific agent that can handle
|
||||
the message of the type T."""
|
||||
|
||||
message: Any
|
||||
sender: AgentId | None
|
||||
recipient: AgentId
|
||||
future: Future[Any]
|
||||
cancellation_token: CancellationToken
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ResponseMessageEnvelope:
|
||||
"""A message envelope for sending a response to a message."""
|
||||
|
||||
message: Any
|
||||
future: Future[Any]
|
||||
sender: AgentId
|
||||
recipient: AgentId | None
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
T = TypeVar("T", bound=Agent)
|
||||
|
||||
|
||||
class QueueAsyncIterable(AsyncIterator[Any], AsyncIterable[Any]):
|
||||
def __init__(self, queue: asyncio.Queue[Any]) -> None:
|
||||
self._queue = queue
|
||||
|
||||
async def __anext__(self) -> Any:
|
||||
return await self._queue.get()
|
||||
|
||||
def __aiter__(self) -> AsyncIterator[Any]:
|
||||
return self
|
||||
|
||||
|
||||
class RuntimeConnection:
|
||||
DEFAULT_GRPC_CONFIG: ClassVar[Mapping[str, Any]] = {
|
||||
"methodConfig": [
|
||||
{
|
||||
"name": [{}],
|
||||
"retryPolicy": {
|
||||
"maxAttempts": 3,
|
||||
"initialBackoff": "0.01s",
|
||||
"maxBackoff": "5s",
|
||||
"backoffMultiplier": 2,
|
||||
"retryableStatusCodes": ["UNAVAILABLE"],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, channel: grpc.aio.Channel) -> None: # type: ignore
|
||||
self._channel = channel
|
||||
self._send_queue = asyncio.Queue[Message]()
|
||||
self._recv_queue = asyncio.Queue[Message]()
|
||||
self._connection_task: Task[None] | None = None
|
||||
|
||||
@classmethod
|
||||
async def from_connection_string(
|
||||
cls, connection_string: str, grpc_config: Mapping[str, Any] = DEFAULT_GRPC_CONFIG
|
||||
) -> Self:
|
||||
channel = grpc.aio.insecure_channel(
|
||||
connection_string, options=[("grpc.service_config", json.dumps(grpc_config))]
|
||||
)
|
||||
await channel.channel_ready()
|
||||
instance = cls(channel)
|
||||
instance._connection_task = asyncio.create_task(
|
||||
instance._connect(channel, instance._send_queue, instance._recv_queue)
|
||||
)
|
||||
return instance
|
||||
|
||||
async def close(self) -> None:
|
||||
await self._channel.close()
|
||||
if self._connection_task is not None:
|
||||
await self._connection_task
|
||||
|
||||
@staticmethod
|
||||
async def _connect( # type: ignore
|
||||
channel: grpc.aio.Channel, send_queue: asyncio.Queue[Message], receive_queue: asyncio.Queue[Message]
|
||||
) -> None:
|
||||
stub: AgentRpcAsyncStub = AgentRpcStub(channel) # type: ignore
|
||||
|
||||
# TODO: where do exceptions from reading the iterable go? How do we recover from those?
|
||||
recv_stream: StreamStreamCall[Message, Message] = stub.OpenChannel(QueueAsyncIterable(send_queue)) # type: ignore
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = await recv_stream.read() # type: ignore
|
||||
if message == grpc.aio.EOF: # type: ignore
|
||||
logger.info("EOF")
|
||||
break
|
||||
message = cast(Message, message)
|
||||
logger.info("Received message: %s", message)
|
||||
await receive_queue.put(message)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
recv_stream = stub.OpenChannel(QueueAsyncIterable(send_queue)) # type: ignore
|
||||
|
||||
async def send(self, message: Message) -> None:
|
||||
await self._send_queue.put(message)
|
||||
|
||||
async def recv(self) -> Message:
|
||||
return await self._recv_queue.get()
|
||||
|
||||
|
||||
class WorkerAgentRuntime(AgentRuntime):
|
||||
def __init__(self) -> None:
|
||||
self._message_queue: List[PublishMessageEnvelope | SendMessageEnvelope | ResponseMessageEnvelope] = []
|
||||
# (namespace, type) -> List[AgentId]
|
||||
self._per_type_subscribers: DefaultDict[tuple[str, type], Set[AgentId]] = defaultdict(set)
|
||||
self._agent_factories: Dict[str, Callable[[], Agent] | Callable[[AgentRuntime, AgentId], Agent]] = {}
|
||||
# If empty, then all namespaces are valid for that agent type
|
||||
self._valid_namespaces: Dict[str, Sequence[str]] = {}
|
||||
self._instantiated_agents: Dict[AgentId, Agent] = {}
|
||||
self._known_namespaces: set[str] = set()
|
||||
self._read_task: None | Task[None] = None
|
||||
self._running = False
|
||||
self._serialization = Serialization()
|
||||
self._pending_requests: Dict[str, Future[Any]] = {}
|
||||
self._pending_requests_lock = threading.Lock()
|
||||
self._next_request_id = 0
|
||||
self._runtime_connection: RuntimeConnection | None = None
|
||||
|
||||
async def setup_channel(self, connection_string: str) -> None:
|
||||
self._runtime_connection = await RuntimeConnection.from_connection_string(connection_string)
|
||||
if self._read_task is None:
|
||||
self._read_task = asyncio.create_task(self.run_read_loop())
|
||||
self._running = True
|
||||
|
||||
async def send_register_agent_type(self, agent_type: str) -> None:
|
||||
assert self._runtime_connection is not None
|
||||
message = Message(registerAgentType=RegisterAgentType(type=agent_type))
|
||||
await self._runtime_connection.send(message)
|
||||
logger.info("Sent registerAgentType message for %s", agent_type)
|
||||
|
||||
async def run_read_loop(self) -> None:
|
||||
# TODO: catch exceptions and reconnect
|
||||
while self._running:
|
||||
message = await self._runtime_connection.recv() # type: ignore
|
||||
logger.info("Got message: %s", message)
|
||||
oneofcase = Message.WhichOneof(message, "message")
|
||||
match oneofcase:
|
||||
case "registerAgentType":
|
||||
logger.warn("Cant handle registerAgentType")
|
||||
case "request":
|
||||
# request: RpcRequest = message.request
|
||||
# source = AgentId(request.source.name, request.source.namespace)
|
||||
# target = AgentId(request.target.name, request.target.namespace)
|
||||
|
||||
raise NotImplementedError("Sending messages is not yet implemented.")
|
||||
case "response":
|
||||
response: RpcResponse = message.response
|
||||
future = self._pending_requests.pop(response.request_id)
|
||||
if len(response.error) > 0:
|
||||
future.set_exception(Exception(response.error))
|
||||
break
|
||||
future.set_result(response.result)
|
||||
case "event":
|
||||
event: Event = message.event
|
||||
message = self._serialization.deserialize(event.data, type_name=event.type)
|
||||
namespace = event.namespace
|
||||
|
||||
for agent_id in self._per_type_subscribers[(namespace, type(message))]:
|
||||
agent = self._get_agent(agent_id)
|
||||
try:
|
||||
await agent.on_message(message, CancellationToken())
|
||||
except Exception as e:
|
||||
event_logger.error("Error handling message", exc_info=e)
|
||||
|
||||
logger.warn("Cant handle event")
|
||||
case None:
|
||||
logger.warn("No message")
|
||||
|
||||
async def close_channel(self) -> None:
|
||||
self._running = False
|
||||
if self._runtime_connection is not None:
|
||||
await self._runtime_connection.close()
|
||||
if self._read_task is not None:
|
||||
await self._read_task
|
||||
|
||||
@property
|
||||
def _known_agent_names(self) -> Set[str]:
|
||||
return set(self._agent_factories.keys())
|
||||
|
||||
# Returns the response of the message
|
||||
async def send_message(
|
||||
self,
|
||||
message: Any,
|
||||
recipient: AgentId,
|
||||
*,
|
||||
sender: AgentId | None = None,
|
||||
cancellation_token: CancellationToken | None = None,
|
||||
) -> Any:
|
||||
assert self._runtime_connection is not None
|
||||
# create a new future for the result
|
||||
future = asyncio.get_event_loop().create_future()
|
||||
with self._pending_requests_lock:
|
||||
self._next_request_id += 1
|
||||
request_id = self._next_request_id
|
||||
request_id_str = str(request_id)
|
||||
self._pending_requests[request_id_str] = future
|
||||
sender = cast(AgentId, sender)
|
||||
runtime_message = Message(
|
||||
request=RpcRequest(
|
||||
request_id=request_id_str,
|
||||
target=AgentIdProto(name=recipient.name, namespace=recipient.namespace),
|
||||
source=AgentIdProto(name=sender.name, namespace=sender.namespace),
|
||||
data=message,
|
||||
)
|
||||
)
|
||||
# TODO: Find a way to handle timeouts/errors
|
||||
asyncio.create_task(self._runtime_connection.send(runtime_message))
|
||||
return await future
|
||||
|
||||
async def publish_message(
|
||||
self,
|
||||
message: Any,
|
||||
*,
|
||||
namespace: str | None = None,
|
||||
sender: AgentId | None = None,
|
||||
cancellation_token: CancellationToken | None = None,
|
||||
) -> None:
|
||||
assert self._runtime_connection is not None
|
||||
sender_namespace = sender.namespace if sender is not None else None
|
||||
explicit_namespace = namespace
|
||||
if explicit_namespace is not None and sender_namespace is not None and explicit_namespace != sender_namespace:
|
||||
raise ValueError(
|
||||
f"Explicit namespace {explicit_namespace} does not match sender namespace {sender_namespace}"
|
||||
)
|
||||
|
||||
assert explicit_namespace is not None or sender_namespace is not None
|
||||
actual_namespace = cast(str, explicit_namespace or sender_namespace)
|
||||
self._process_seen_namespace(actual_namespace)
|
||||
message_type = self._serialization.type_name(message)
|
||||
serialized_message = self._serialization.serialize(message, type_name=message_type)
|
||||
message = Message(event=Event(namespace=actual_namespace, type=message_type, data=serialized_message))
|
||||
|
||||
async def write_message() -> None:
|
||||
assert self._runtime_connection is not None
|
||||
await self._runtime_connection.send(message)
|
||||
|
||||
await asyncio.create_task(write_message())
|
||||
|
||||
def save_state(self) -> Mapping[str, Any]:
|
||||
raise NotImplementedError("Saving state is not yet implemented.")
|
||||
|
||||
def load_state(self, state: Mapping[str, Any]) -> None:
|
||||
raise NotImplementedError("Loading state is not yet implemented.")
|
||||
|
||||
def agent_metadata(self, agent: AgentId) -> AgentMetadata:
|
||||
raise NotImplementedError("Agent metadata is not yet implemented.")
|
||||
|
||||
def agent_save_state(self, agent: AgentId) -> Mapping[str, Any]:
|
||||
raise NotImplementedError("Agent save_state is not yet implemented.")
|
||||
|
||||
def agent_load_state(self, agent: AgentId, state: Mapping[str, Any]) -> None:
|
||||
raise NotImplementedError("Agent load_state is not yet implemented.")
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
agent_factory: Callable[[], T] | Callable[[AgentRuntime, AgentId], T],
|
||||
) -> None:
|
||||
if name in self._agent_factories:
|
||||
raise ValueError(f"Agent with name {name} already exists.")
|
||||
self._agent_factories[name] = agent_factory
|
||||
|
||||
# For all already prepared namespaces we need to prepare this agent
|
||||
for namespace in self._known_namespaces:
|
||||
self._get_agent(AgentId(name=name, namespace=namespace))
|
||||
|
||||
# TODO do we need to convert register to async?
|
||||
asyncio.create_task(self.send_register_agent_type(name))
|
||||
|
||||
def _invoke_agent_factory(
|
||||
self,
|
||||
agent_factory: Callable[[], T] | Callable[[AgentRuntime, AgentId], T],
|
||||
agent_id: AgentId,
|
||||
) -> T:
|
||||
if len(inspect.signature(agent_factory).parameters) == 0:
|
||||
factory_one = cast(Callable[[], T], agent_factory)
|
||||
agent = factory_one()
|
||||
elif len(inspect.signature(agent_factory).parameters) == 2:
|
||||
factory_two = cast(Callable[[AgentRuntime, AgentId], T], agent_factory)
|
||||
agent = factory_two(self, agent_id)
|
||||
else:
|
||||
raise ValueError("Agent factory must take 0 or 2 arguments.")
|
||||
|
||||
return agent
|
||||
|
||||
def _get_agent(self, agent_id: AgentId) -> Agent:
|
||||
self._process_seen_namespace(agent_id.namespace)
|
||||
if agent_id in self._instantiated_agents:
|
||||
return self._instantiated_agents[agent_id]
|
||||
|
||||
if agent_id.name not in self._agent_factories:
|
||||
raise ValueError(f"Agent with name {agent_id.name} not found.")
|
||||
|
||||
agent_factory = self._agent_factories[agent_id.name]
|
||||
|
||||
agent = self._invoke_agent_factory(agent_factory, agent_id)
|
||||
for message_type in agent.metadata["subscriptions"]:
|
||||
self._per_type_subscribers[(agent_id.namespace, message_type)].add(agent_id)
|
||||
self._serialization.add_type(message_type)
|
||||
|
||||
self._instantiated_agents[agent_id] = agent
|
||||
return agent
|
||||
|
||||
def get(self, name: str, *, namespace: str = "default") -> AgentId:
|
||||
return self._get_agent(AgentId(name=name, namespace=namespace)).id
|
||||
|
||||
def get_proxy(self, name: str, *, namespace: str = "default") -> AgentProxy:
|
||||
id = self.get(name, namespace=namespace)
|
||||
return AgentProxy(id, self)
|
||||
|
||||
# Hydrate the agent instances in a namespace. The primary reason for this is
|
||||
# to ensure message type subscriptions are set up.
|
||||
def _process_seen_namespace(self, namespace: str) -> None:
|
||||
if namespace in self._known_namespaces:
|
||||
return
|
||||
|
||||
self._known_namespaces.add(namespace)
|
||||
for name in self._known_agent_names:
|
||||
self._get_agent(AgentId(name=name, namespace=namespace))
|
||||
|
||||
def add_serialization_type(self, message_type: type) -> None:
|
||||
self._serialization.add_type(message_type)
|
|
@ -0,0 +1,42 @@
|
|||
from agnext.worker.worker_runtime import WorkerAgentRuntime
|
||||
from agnext.components import TypeRoutedAgent, message_handler
|
||||
from agnext.core import CancellationToken, AgentId
|
||||
import logging
|
||||
import asyncio
|
||||
#import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class ExampleMessagePayload:
|
||||
content: str
|
||||
|
||||
|
||||
class ExampleAgent(TypeRoutedAgent):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Example Agent")
|
||||
|
||||
@message_handler
|
||||
async def on_example_payload(self, message: ExampleMessagePayload, cancellation_token: CancellationToken) -> None:
|
||||
upper_case = message.content.upper()
|
||||
await self.publish_message(ExampleMessagePayload(content=upper_case))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
logger = logging.getLogger("main")
|
||||
runtime = WorkerAgentRuntime()
|
||||
await runtime.setup_channel("localhost:5438") #os.environ["AGENT_HOST"])
|
||||
|
||||
runtime.register("ExampleAgent", lambda: ExampleAgent())
|
||||
while True:
|
||||
try:
|
||||
res = await runtime.send_message("testing!", recipient=AgentId(name="greeter", namespace="testing"), sender=AgentId(name="ExampleAgent", namespace="testing"))
|
||||
logger.info("Response: %s", res)
|
||||
except Exception as e:
|
||||
logger.warning("Error: %s", e)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
asyncio.run(main())
|
||||
|
Loading…
Reference in New Issue