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:
Reuben Bond 2024-06-28 08:03:42 -07:00 committed by GitHub
parent 13b0d0deb4
commit ebed669231
71 changed files with 2379 additions and 62 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
{
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
namespace Marketing.Options;
namespace Marketing.Options;
public static class Consts
{
public const string OrleansNamespace = "DevPersonas";
public const string OrleansNamespace = "default";
}

View File

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

View File

@ -0,0 +1,3 @@
qdrant
orleans
openai

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

49
protos/agent_worker.proto Normal file
View File

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

3
python/.gitignore vendored
View File

@ -163,3 +163,6 @@ cython_debug/
/docs/src/reference
.DS_Store
# Generated proto files
src/agnext/worker/protos/agent*

16
python/.vscode/launch.json vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

42
python/worker_example.py Normal file
View File

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