From 16b20f26f9e22f9650e6e14e25d3f72bad746ed0 Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Tue, 28 Jan 2025 18:23:46 -0500 Subject: [PATCH 01/25] feat: Set up Scaffolding for Core.Grpc * Define skeleton for GrpcAgentRuntime * Implement CloudEvent and RPC Payload serialization/marshaling --- .../Core.Grpc/GrpcAgentRuntime.cs | 597 ++++++++++++++++++ .../GrpcAgentWorkerHostBuilderExtension.cs | 70 ++ .../Core.Grpc/IAgentMessageSerializer.cs | 23 + .../Core.Grpc/IAgentRuntimeExtensions.cs | 102 +++ .../Core.Grpc/IProtoMessageSerializer.cs | 10 + .../Core.Grpc/ISerializationRegistry.cs | 27 + .../Core.Grpc/ITypeNameResolver.cs | 9 + .../Core.Grpc/ProtoSerializationRegistry.cs | 37 ++ .../Core.Grpc/ProtoTypeNameResolver.cs | 21 + .../Core.Grpc/ProtobufConversionExtensions.cs | 61 ++ .../Core.Grpc/ProtobufMessageSerializer.cs | 46 ++ .../src/Microsoft.AutoGen/Core/AgentsApp.cs | 2 + 12 files changed, 1005 insertions(+) create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentMessageSerializer.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentRuntimeExtensions.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtoMessageSerializer.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/ISerializationRegistry.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoSerializationRegistry.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoTypeNameResolver.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufConversionExtensions.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufMessageSerializer.cs diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs new file mode 100644 index 000000000000..5deba58ae62b --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -0,0 +1,597 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GrpcAgentRuntime.cs + +using System.Collections.Concurrent; +using System.Threading.Channels; +using Google.Protobuf; +using Grpc.Core; +using Microsoft.AutoGen.Contracts; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.AutoGen.Protobuf; + +namespace Microsoft.AutoGen.Core.Grpc; + +public sealed class GrpcAgentRuntime( + AgentRpc.AgentRpcClient client, + IHostApplicationLifetime hostApplicationLifetime, + IServiceProvider serviceProvider, + ILogger logger + ) : IAgentRuntime, IDisposable +{ + private readonly object _channelLock = new(); + + // Request ID -> + private readonly ConcurrentDictionary> _pendingRequests = new(); + private Dictionary>> agentFactories = new(); + private Dictionary agentInstances = new(); + + private readonly Channel<(Message Message, TaskCompletionSource WriteCompletionSource)> _outboundMessagesChannel = Channel.CreateBounded<(Message, TaskCompletionSource)>(new BoundedChannelOptions(1024) + { + AllowSynchronousContinuations = true, + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.Wait + }); + + private readonly AgentRpc.AgentRpcClient _client = client; + public readonly IServiceProvider ServiceProvider = serviceProvider; + + private readonly ILogger _logger = logger; + private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); + private AsyncDuplexStreamingCall? _channel; + private Task? _readTask; + private Task? _writeTask; + + private string _clientId = Guid.NewGuid().ToString(); + private CallOptions CallOptions + { + get + { + var metadata = new Metadata + { + { "client-id", this._clientId } + }; + return new CallOptions(headers: metadata); + } + } + + public IProtoSerializationRegistry SerializationRegistry { get; } = new ProtoSerializationRegistry(); + + public void Dispose() + { + _outboundMessagesChannel.Writer.TryComplete(); + _channel?.Dispose(); + } + + private async Task RunReadPump() + { + var channel = GetChannel(); + while (!_shutdownCts.Token.IsCancellationRequested) + { + try + { + await foreach (var message in channel.ResponseStream.ReadAllAsync(_shutdownCts.Token)) + { + // next if message is null + if (message == null) + { + continue; + } + switch (message.MessageCase) + { + case Message.MessageOneofCase.Request: + var request = message.Request ?? throw new InvalidOperationException("Request is null."); + await HandleRequest(request); + break; + case Message.MessageOneofCase.Response: + var response = message.Response ?? throw new InvalidOperationException("Response is null."); + await HandleResponse(response); + break; + case Message.MessageOneofCase.CloudEvent: + var cloudEvent = message.CloudEvent ?? throw new InvalidOperationException("CloudEvent is null."); + await HandlePublish(cloudEvent); + break; + default: + throw new InvalidOperationException($"Unexpected message '{message}'."); + } + } + } + catch (OperationCanceledException) + { + // Time to shut down. + break; + } + catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) + { + _logger.LogError(ex, "Error reading from channel."); + channel = RecreateChannel(channel); + } + catch + { + // Shutdown requested. + break; + } + } + } + + private async ValueTask HandleRequest(RpcRequest request, CancellationToken cancellationToken = default) + { + if (request is null) + { + throw new InvalidOperationException("Request is null."); + } + if (request.Payload is null) + { + throw new InvalidOperationException("Payload is null."); + } + if (request.Target is null) + { + throw new InvalidOperationException("Target is null."); + } + if (request.Source is null) + { + throw new InvalidOperationException("Source is null."); + } + + var agentId = request.Target; + var agent = await EnsureAgentAsync(agentId.FromProtobuf()); + + // Convert payload back to object + var payload = request.Payload; + var message = PayloadToObject(payload); + + var messageContext = new MessageContext(request.RequestId, cancellationToken) + { + Sender = request.Source.FromProtobuf(), + Topic = null, + IsRpc = true + }; + + var result = await agent.OnMessageAsync(message, messageContext); + + if (result is not null) + { + var response = new RpcResponse + { + RequestId = request.RequestId, + Payload = ObjectToPayload(result) + }; + + var responseMessage = new Message + { + Response = response + }; + + await WriteChannelAsync(responseMessage, cancellationToken); + } + } + + private async ValueTask HandleResponse(RpcResponse request, CancellationToken _ = default) + { + if (request is null) + { + throw new InvalidOperationException("Request is null."); + } + if (request.Payload is null) + { + throw new InvalidOperationException("Payload is null."); + } + if (request.RequestId is null) + { + throw new InvalidOperationException("RequestId is null."); + } + + if (_pendingRequests.TryRemove(request.RequestId, out var resultSink)) + { + var payload = request.Payload; + var message = PayloadToObject(payload); + resultSink.SetResult(message); + } + } + + private async ValueTask HandlePublish(CloudEvent evt, CancellationToken cancellationToken = default) + { + if (evt is null) + { + throw new InvalidOperationException("CloudEvent is null."); + } + if (evt.ProtoData is null) + { + throw new InvalidOperationException("ProtoData is null."); + } + if (evt.Attributes is null) + { + throw new InvalidOperationException("Attributes is null."); + } + + var topic = new TopicId(evt.Type, evt.Source); + var sender = new Contracts.AgentId + { + Type = evt.Attributes["agagentsendertype"].CeString, + Key = evt.Attributes["agagentsenderkey"].CeString + }; + + var messageId = evt.Id; + var typeName = evt.Attributes["dataschema"].CeString; + var serializer = SerializationRegistry.GetSerializer(typeName) ?? throw new Exception(); + var message = serializer.Deserialize(evt.ProtoData); + + var messageContext = new MessageContext(messageId, cancellationToken) + { + Sender = sender, + Topic = topic, + IsRpc = false + }; + var agent = await EnsureAgentAsync(sender); + await agent.OnMessageAsync(message, messageContext); + } + + private async Task RunWritePump() + { + var channel = GetChannel(); + var outboundMessages = _outboundMessagesChannel.Reader; + while (!_shutdownCts.IsCancellationRequested) + { + (Message Message, TaskCompletionSource WriteCompletionSource) item = default; + try + { + await outboundMessages.WaitToReadAsync().ConfigureAwait(false); + + // Read the next message if we don't already have an unsent message + // waiting to be sent. + if (!outboundMessages.TryRead(out item)) + { + break; + } + + while (!_shutdownCts.IsCancellationRequested) + { + await channel.RequestStream.WriteAsync(item.Message, _shutdownCts.Token).ConfigureAwait(false); + item.WriteCompletionSource.TrySetResult(); + break; + } + } + catch (OperationCanceledException) + { + // Time to shut down. + item.WriteCompletionSource?.TrySetCanceled(); + break; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable) + { + // we could not connect to the endpoint - most likely we have the wrong port or failed ssl + // we need to let the user know what port we tried to connect to and then do backoff and retry + _logger.LogError(ex, "Error connecting to GRPC endpoint {Endpoint}.", Environment.GetEnvironmentVariable("AGENT_HOST")); + break; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.OK) + { + _logger.LogError(ex, "Error writing to channel, continuing (Status OK). {ex}", channel.ToString()); + break; + } + catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) + { + item.WriteCompletionSource?.TrySetException(ex); + _logger.LogError(ex, $"Error writing to channel.{ex}"); + channel = RecreateChannel(channel); + continue; + } + catch + { + // Shutdown requested. + item.WriteCompletionSource?.TrySetCanceled(); + break; + } + } + + while (outboundMessages.TryRead(out var item)) + { + item.WriteCompletionSource.TrySetCanceled(); + } + } + + // private override async ValueTask SendMessageAsync(Payload message, AgentId agentId, AgentId? agent = null, CancellationToken? cancellationToken = default) + // { + // var request = new RpcRequest + // { + // RequestId = Guid.NewGuid().ToString(), + // Source = agent, + // Target = agentId, + // Payload = message, + // }; + + // // Actually send it and wait for the response + // throw new NotImplementedException(); + // } + + // new is intentional + + // public new async ValueTask RuntimeSendRequestAsync(IAgent agent, RpcRequest request, CancellationToken cancellationToken = default) + // { + // var requestId = Guid.NewGuid().ToString(); + // _pendingRequests[requestId] = ((Agent)agent, request.RequestId); + // request.RequestId = requestId; + // await WriteChannelAsync(new Message { Request = request }, cancellationToken).ConfigureAwait(false); + // } + + private async Task WriteChannelAsync(Message message, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + await _outboundMessagesChannel.Writer.WriteAsync((message, tcs), cancellationToken).ConfigureAwait(false); + } + private AsyncDuplexStreamingCall GetChannel() + { + if (_channel is { } channel) + { + return channel; + } + + lock (_channelLock) + { + if (_channel is not null) + { + return _channel; + } + + return RecreateChannel(null); + } + } + + private AsyncDuplexStreamingCall RecreateChannel(AsyncDuplexStreamingCall? channel) + { + if (_channel is null || _channel == channel) + { + lock (_channelLock) + { + if (_channel is null || _channel == channel) + { + _channel?.Dispose(); + _channel = _client.OpenChannel(cancellationToken: _shutdownCts.Token); + } + } + } + + return _channel; + } + public async Task StartAsync(CancellationToken cancellationToken) + { + _channel = GetChannel(); + _logger.LogInformation("Starting " + GetType().Name + ",connecting to gRPC endpoint " + Environment.GetEnvironmentVariable("AGENT_HOST")); + var didSuppress = false; + if (!ExecutionContext.IsFlowSuppressed()) + { + didSuppress = true; + ExecutionContext.SuppressFlow(); + } + + try + { + _readTask = Task.Run(RunReadPump, cancellationToken); + _writeTask = Task.Run(RunWritePump, cancellationToken); + } + finally + { + if (didSuppress) + { + ExecutionContext.RestoreFlow(); + } + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _shutdownCts.Cancel(); + + _outboundMessagesChannel.Writer.TryComplete(); + + if (_readTask is { } readTask) + { + await readTask.ConfigureAwait(false); + } + + if (_writeTask is { } writeTask) + { + await writeTask.ConfigureAwait(false); + } + lock (_channelLock) + { + _channel?.Dispose(); + } + } + + private async ValueTask EnsureAgentAsync(Contracts.AgentId agentId) + { + if (!this.agentInstances.TryGetValue(agentId, out IHostableAgent? agent)) + { + if (!this.agentFactories.TryGetValue(agentId.Type, out Func>? factoryFunc)) + { + throw new Exception($"Agent with name {agentId.Type} not found."); + } + + agent = await factoryFunc(agentId, this); + this.agentInstances.Add(agentId, agent); + } + + return this.agentInstances[agentId]; + } + + private Payload ObjectToPayload(object message) { + if (!SerializationRegistry.Exists(message.GetType())) + { + SerializationRegistry.RegisterSerializer(message.GetType()); + } + var rpcMessage = (SerializationRegistry.GetSerializer(message.GetType()) ?? throw new Exception()).Serialize(message); + + var typeName = SerializationRegistry.TypeNameResolver.ResolveTypeName(message); + const string PAYLOAD_DATA_CONTENT_TYPE = "application/x-protobuf"; + + // Protobuf any to byte array + Payload payload = new() + { + DataType = typeName, + DataContentType = PAYLOAD_DATA_CONTENT_TYPE, + Data = rpcMessage.ToByteString() + }; + + return payload; + } + + private object PayloadToObject(Payload payload) { + var typeName = payload.DataType; + var data = payload.Data; + var type = SerializationRegistry.TypeNameResolver.ResolveTypeName(typeName); + var serializer = SerializationRegistry.GetSerializer(type) ?? throw new Exception(); + var any = Google.Protobuf.WellKnownTypes.Any.Parser.ParseFrom(data); + return serializer.Deserialize(any); + } + + public async ValueTask SendMessageAsync(object message, Contracts.AgentId recepient, Contracts.AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + { + if (!SerializationRegistry.Exists(message.GetType())) + { + SerializationRegistry.RegisterSerializer(message.GetType()); + } + + var payload = ObjectToPayload(message); + var request = new RpcRequest + { + RequestId = Guid.NewGuid().ToString(), + Source = (sender ?? new Contracts.AgentId() ).ToProtobuf(), + Target = recepient.ToProtobuf(), + Payload = payload, + }; + + Message msg = new() + { + Request = request + }; + // Create a future that will be completed when the response is received + var resultSink = new ResultSink(); + this._pendingRequests.TryAdd(request.RequestId, resultSink); + await WriteChannelAsync(msg, cancellationToken); + + return await resultSink.Future; + } + + private CloudEvent CreateCloudEvent(Google.Protobuf.WellKnownTypes.Any payload, TopicId topic, string dataType, Contracts.AgentId sender, string messageId) + { + const string PAYLOAD_DATA_CONTENT_TYPE = "application/x-protobuf"; + return new CloudEvent + { + ProtoData = payload, + Type = topic.Type, + Source = topic.Source, + Id = messageId, + Attributes = { + { + "datacontenttype", new CloudEvent.Types.CloudEventAttributeValue { CeString = PAYLOAD_DATA_CONTENT_TYPE } + }, + { + "dataschema", new CloudEvent.Types.CloudEventAttributeValue { CeString = dataType } + }, + { + "agagentsendertype", new CloudEvent.Types.CloudEventAttributeValue { CeString = sender.Type } + }, + { + "agagentsenderkey", new CloudEvent.Types.CloudEventAttributeValue { CeString = sender.Key } + }, + { + "agmsgkind", new CloudEvent.Types.CloudEventAttributeValue { CeString = "publish" } + } + } + }; + } + + public async ValueTask PublishMessageAsync(object message, TopicId topic, Contracts.AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + { + if (!SerializationRegistry.Exists(message.GetType())) + { + SerializationRegistry.RegisterSerializer(message.GetType()); + } + var protoAny = (SerializationRegistry.GetSerializer(message.GetType()) ?? throw new Exception()).Serialize(message); + var typeName = SerializationRegistry.TypeNameResolver.ResolveTypeName(message); + + var cloudEvent = CreateCloudEvent(protoAny, topic, typeName, sender ?? new Contracts.AgentId(), messageId ?? Guid.NewGuid().ToString()); + + Message msg = new() + { + CloudEvent = cloudEvent + }; + await WriteChannelAsync(msg, cancellationToken); + } + + public ValueTask GetAgentAsync(Contracts.AgentId agentId, bool lazy = true) + { + throw new NotImplementedException(); + } + + public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true) + { + throw new NotImplementedException(); + } + + public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true) + { + throw new NotImplementedException(); + } + + public ValueTask> SaveAgentStateAsync(Contracts.AgentId agentId) + { + throw new NotImplementedException(); + } + + public ValueTask LoadAgentStateAsync(Contracts.AgentId agentId, IDictionary state) + { + throw new NotImplementedException(); + } + + public ValueTask GetAgentMetadataAsync(Contracts.AgentId agentId) + { + throw new NotImplementedException(); + } + + public async ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription) + { + var _ = await this._client.AddSubscriptionAsync(new AddSubscriptionRequest{ + Subscription = subscription.ToProtobuf() + },this.CallOptions); + } + + public ValueTask RemoveSubscriptionAsync(string subscriptionId) + { + throw new NotImplementedException(); + } + + public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) + { + if (this.agentFactories.ContainsKey(type)) + { + throw new Exception($"Agent with type {type} already exists."); + } + this.agentFactories.Add(type, async (agentId, runtime) => await factoryFunc(agentId, runtime)); + + this._client.RegisterAgentAsync(new RegisterAgentTypeRequest + { + Type = type.Name, + + }, this.CallOptions); + return ValueTask.FromResult(type); + } + + public ValueTask TryGetAgentProxyAsync(Contracts.AgentId agentId) + { + throw new NotImplementedException(); + } + + public ValueTask> SaveStateAsync() + { + throw new NotImplementedException(); + } + + public ValueTask LoadStateAsync(IDictionary state) + { + throw new NotImplementedException(); + } +} + diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs new file mode 100644 index 000000000000..7f43b9620f54 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GrpcAgentWorkerHostBuilderExtension.cs +using System.Diagnostics; +using Grpc.Core; +using Grpc.Net.Client.Configuration; +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Protobuf; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +namespace Microsoft.AutoGen.Core.Grpc; + +public static class GrpcAgentWorkerHostBuilderExtensions +{ + private const string _defaultAgentServiceAddress = "https://localhost:53071"; + + // TODO: How do we ensure AddGrpcAgentWorker and UseInProcessRuntime are mutually exclusive? + public static AgentsAppBuilder AddGrpcAgentWorker(this AgentsAppBuilder builder, string? agentServiceAddress = null) + { + builder.Services.AddGrpcClient(options => + { + options.Address = new Uri(agentServiceAddress ?? builder.Configuration["AGENT_HOST"] ?? _defaultAgentServiceAddress); + options.ChannelOptionsActions.Add(channelOptions => + { + var loggerFactory = new LoggerFactory(); + if (Debugger.IsAttached) + { + channelOptions.HttpHandler = new SocketsHttpHandler + { + EnableMultipleHttp2Connections = false, + KeepAlivePingDelay = TimeSpan.FromSeconds(200), + KeepAlivePingTimeout = TimeSpan.FromSeconds(100), + KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always + }; + } + else + { + channelOptions.HttpHandler = new SocketsHttpHandler + { + EnableMultipleHttp2Connections = true, + KeepAlivePingDelay = TimeSpan.FromSeconds(20), + KeepAlivePingTimeout = TimeSpan.FromSeconds(10), + KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests + }; + } + + var methodConfig = new MethodConfig + { + Names = { MethodName.Default }, + RetryPolicy = new RetryPolicy + { + MaxAttempts = 5, + InitialBackoff = TimeSpan.FromSeconds(1), + MaxBackoff = TimeSpan.FromSeconds(5), + BackoffMultiplier = 1.5, + RetryableStatusCodes = { StatusCode.Unavailable } + } + }; + + channelOptions.ServiceConfig = new() { MethodConfigs = { methodConfig } }; + channelOptions.ThrowOperationCanceledOnCancellation = true; + }); + }); + builder.Services.TryAddSingleton(DistributedContextPropagator.Current); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => (IHostedService)sp.GetRequiredService()); + return builder; + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentMessageSerializer.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentMessageSerializer.cs new file mode 100644 index 000000000000..0cc422d54d85 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentMessageSerializer.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IAgentMessageSerializer.cs + +namespace Microsoft.AutoGen.Core.Grpc; +/// +/// Interface for serializing and deserializing agent messages. +/// +public interface IAgentMessageSerializer +{ + /// + /// Serialize an agent message. + /// + /// The message to serialize. + /// The serialized message. + Google.Protobuf.WellKnownTypes.Any Serialize(object message); + + /// + /// Deserialize an agent message. + /// + /// The message to deserialize. + /// The deserialized message. + object Deserialize(Google.Protobuf.WellKnownTypes.Any message); +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentRuntimeExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentRuntimeExtensions.cs new file mode 100644 index 000000000000..8179ff4b494b --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentRuntimeExtensions.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IAgentRuntimeExtensions.cs + +using System.Diagnostics; +using Google.Protobuf.Collections; +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Protobuf; +using Microsoft.Extensions.DependencyInjection; +using static Microsoft.AutoGen.Contracts.CloudEvent.Types; + +namespace Microsoft.AutoGen.Core.Grpc; + +public static class GrpcAgentRuntimeExtensions +{ + public static (string?, string?) GetTraceIdAndState(GrpcAgentRuntime runtime, IDictionary metadata) + { + var dcp = runtime.ServiceProvider.GetRequiredService(); + dcp.ExtractTraceIdAndState(metadata, + static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => + { + var metadata = (IDictionary)carrier!; + fieldValues = null; + metadata.TryGetValue(fieldName, out fieldValue); + }, + out var traceParent, + out var traceState); + return (traceParent, traceState); + } + public static (string?, string?) GetTraceIdAndState(GrpcAgentRuntime worker, MapField metadata) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + dcp.ExtractTraceIdAndState(metadata, + static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => + { + var metadata = (MapField)carrier!; + fieldValues = null; + metadata.TryGetValue(fieldName, out var ceValue); + fieldValue = ceValue?.CeString; + }, + out var traceParent, + out var traceState); + return (traceParent, traceState); + } + public static void Update(GrpcAgentRuntime worker, RpcRequest request, Activity? activity = null) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + dcp.Inject(activity, request.Metadata, static (carrier, key, value) => + { + var metadata = (IDictionary)carrier!; + if (metadata.TryGetValue(key, out _)) + { + metadata[key] = value; + } + else + { + metadata.Add(key, value); + } + }); + } + public static void Update(GrpcAgentRuntime worker, CloudEvent cloudEvent, Activity? activity = null) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + dcp.Inject(activity, cloudEvent.Attributes, static (carrier, key, value) => + { + var mapField = (MapField)carrier!; + if (mapField.TryGetValue(key, out var ceValue)) + { + mapField[key] = new CloudEventAttributeValue { CeString = value }; + } + else + { + mapField.Add(key, new CloudEventAttributeValue { CeString = value }); + } + }); + } + + public static IDictionary ExtractMetadata(GrpcAgentRuntime worker, IDictionary metadata) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + var baggage = dcp.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => + { + var metadata = (IDictionary)carrier!; + fieldValues = null; + metadata.TryGetValue(fieldName, out fieldValue); + }); + + return baggage as IDictionary ?? new Dictionary(); + } + public static IDictionary ExtractMetadata(GrpcAgentRuntime worker, MapField metadata) + { + var dcp = worker.ServiceProvider.GetRequiredService(); + var baggage = dcp.ExtractBaggage(metadata, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable? fieldValues) => + { + var metadata = (MapField)carrier!; + fieldValues = null; + metadata.TryGetValue(fieldName, out var ceValue); + fieldValue = ceValue?.CeString; + }); + + return baggage as IDictionary ?? new Dictionary(); + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtoMessageSerializer.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtoMessageSerializer.cs new file mode 100644 index 000000000000..ca690e508d2b --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtoMessageSerializer.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IProtoMessageSerializer.cs + +namespace Microsoft.AutoGen.Core.Grpc; + +public interface IProtoMessageSerializer +{ + Google.Protobuf.WellKnownTypes.Any Serialize(object input); + object Deserialize(Google.Protobuf.WellKnownTypes.Any input); +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ISerializationRegistry.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ISerializationRegistry.cs new file mode 100644 index 000000000000..190ed3ec239d --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ISerializationRegistry.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ISerializationRegistry.cs + +namespace Microsoft.AutoGen.Core.Grpc; + +public interface IProtoSerializationRegistry +{ + /// + /// Registers a serializer for the specified type. + /// + /// The type to register. + void RegisterSerializer(System.Type type) => RegisterSerializer(type, new ProtobufMessageSerializer(type)); + + void RegisterSerializer(System.Type type, IProtoMessageSerializer serializer); + + /// + /// Gets the serializer for the specified type. + /// + /// The type to get the serializer for. + /// The serializer for the specified type. + IProtoMessageSerializer? GetSerializer(System.Type type) => GetSerializer(TypeNameResolver.ResolveTypeName(type)); + IProtoMessageSerializer? GetSerializer(string typeName); + + ITypeNameResolver TypeNameResolver { get; } + + bool Exists(System.Type type); +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs new file mode 100644 index 000000000000..24de4cb8b449 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ITypeNameResolver.cs + +namespace Microsoft.AutoGen.Core.Grpc; + +public interface ITypeNameResolver +{ + string ResolveTypeName(object input); +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoSerializationRegistry.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoSerializationRegistry.cs new file mode 100644 index 000000000000..e744bcb0eee9 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoSerializationRegistry.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ProtoSerializationRegistry.cs + +namespace Microsoft.AutoGen.Core.Grpc; + +public class ProtoSerializationRegistry : IProtoSerializationRegistry +{ + private readonly Dictionary _serializers + = new Dictionary(); + + public ITypeNameResolver TypeNameResolver => new ProtoTypeNameResolver(); + + public bool Exists(Type type) + { + return _serializers.ContainsKey(TypeNameResolver.ResolveTypeName(type)); + } + + public IProtoMessageSerializer? GetSerializer(Type type) + { + return GetSerializer(TypeNameResolver.ResolveTypeName(type)); + } + + public IProtoMessageSerializer? GetSerializer(string typeName) + { + _serializers.TryGetValue(typeName, out var serializer); + return serializer; + } + + public void RegisterSerializer(Type type, IProtoMessageSerializer serializer) + { + if (_serializers.ContainsKey(TypeNameResolver.ResolveTypeName(type))) + { + throw new InvalidOperationException($"Serializer already registered for {type.FullName}"); + } + _serializers[TypeNameResolver.ResolveTypeName(type)] = serializer; + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoTypeNameResolver.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoTypeNameResolver.cs new file mode 100644 index 000000000000..a769b0f31c81 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoTypeNameResolver.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ProtoTypeNameResolver.cs + +using Google.Protobuf; + +namespace Microsoft.AutoGen.Core.Grpc; + +public class ProtoTypeNameResolver : ITypeNameResolver +{ + public string ResolveTypeName(object input) + { + if (input is IMessage protoMessage) + { + return protoMessage.Descriptor.FullName; + } + else + { + throw new ArgumentException("Input must be a protobuf message."); + } + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufConversionExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufConversionExtensions.cs new file mode 100644 index 000000000000..4850b7825afe --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufConversionExtensions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ProtobufConversionExtensions.cs + +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Protobuf; + +namespace Microsoft.AutoGen.Core.Grpc; + +public static class ProtobufConversionExtensions +{ + // Convert an ISubscrptionDefinition to a Protobuf Subscription + public static Subscription? ToProtobuf(this ISubscriptionDefinition subscriptionDefinition) + { + // Check if is a TypeSubscription + if (subscriptionDefinition is Contracts.TypeSubscription typeSubscription) + { + return new Subscription + { + Id = typeSubscription.Id, + TypeSubscription = new Protobuf.TypeSubscription + { + TopicType = typeSubscription.TopicType, + AgentType = typeSubscription.AgentType + } + }; + } + + // Check if is a TypePrefixSubscription + if (subscriptionDefinition is Contracts.TypePrefixSubscription typePrefixSubscription) + { + return new Subscription + { + Id = typePrefixSubscription.Id, + TypePrefixSubscription = new Protobuf.TypePrefixSubscription + { + TopicTypePrefix = typePrefixSubscription.TopicTypePrefix, + AgentType = typePrefixSubscription.AgentType + } + }; + } + + return null; + } + + // Convert AgentId from Protobuf to AgentId + public static Contracts.AgentId FromProtobuf(this Protobuf.AgentId agentId) + { + return new Contracts.AgentId(agentId.Type, agentId.Key); + } + + // Convert AgentId from AgentId to Protobuf + public static Protobuf.AgentId ToProtobuf(this Contracts.AgentId agentId) + { + return new Protobuf.AgentId + { + Type = agentId.Type, + Key = agentId.Key + }; + } + +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufMessageSerializer.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufMessageSerializer.cs new file mode 100644 index 000000000000..55c1aebfa47d --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufMessageSerializer.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ProtobufMessageSerializer.cs + +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; + +namespace Microsoft.AutoGen.Core.Grpc; + +/// +/// Interface for serializing and deserializing agent messages. +/// +public class ProtobufMessageSerializer : IProtoMessageSerializer +{ + private System.Type _concreteType; + + public ProtobufMessageSerializer(System.Type concreteType) + { + _concreteType = concreteType; + } + + public object Deserialize(Any message) + { + // Check if the concrete type is a proto IMessage + if (typeof(IMessage).IsAssignableFrom(_concreteType)) + { + var nameOfMethod = nameof(Any.Unpack); + var result = message.GetType().GetMethods().Where(m => m.Name == nameOfMethod && m.IsGenericMethod).First().MakeGenericMethod(_concreteType).Invoke(message, null); + return result as IMessage ?? throw new ArgumentException("Failed to deserialize", nameof(message)); + } + + // Raise an exception if the concrete type is not a proto IMessage + throw new ArgumentException("Concrete type must be a proto IMessage", nameof(_concreteType)); + } + + public Any Serialize(object message) + { + // Check if message is a proto IMessage + if (message is IMessage protoMessage) + { + return Any.Pack(protoMessage); + } + + // Raise an exception if the message is not a proto IMessage + throw new ArgumentException("Message must be a proto IMessage", nameof(message)); + } +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentsApp.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentsApp.cs index bae09a9f1917..cecd8d9ec48d 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/AgentsApp.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/AgentsApp.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Reflection; using Microsoft.AutoGen.Contracts; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -21,6 +22,7 @@ public AgentsAppBuilder(HostApplicationBuilder? baseBuilder = null) } public IServiceCollection Services => this.builder.Services; + public IConfiguration Configuration => this.builder.Configuration; public void AddAgentsFromAssemblies() { From e5da4375002cfdaaea3401ab7453e269895fd56f Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Wed, 29 Jan 2025 23:03:26 -0500 Subject: [PATCH 02/25] refactor: Extract Message channel logic to MessageRouter --- .../Core.Grpc/GrpcAgentRuntime.cs | 509 +++++++++++------- 1 file changed, 312 insertions(+), 197 deletions(-) diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs index 5deba58ae62b..1797182a9704 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -12,89 +12,130 @@ namespace Microsoft.AutoGen.Core.Grpc; -public sealed class GrpcAgentRuntime( - AgentRpc.AgentRpcClient client, - IHostApplicationLifetime hostApplicationLifetime, - IServiceProvider serviceProvider, - ILogger logger - ) : IAgentRuntime, IDisposable +// TODO: Consider whether we want to just reuse IHandle +internal interface IMessageSink { - private readonly object _channelLock = new(); + public ValueTask OnMessageAsync(TMessage message, CancellationToken cancellation = default); +} - // Request ID -> - private readonly ConcurrentDictionary> _pendingRequests = new(); - private Dictionary>> agentFactories = new(); - private Dictionary agentInstances = new(); +internal sealed class AutoRestartChannel : IDisposable +{ + private readonly object _channelLock = new(); + private readonly AgentRpc.AgentRpcClient _client; + private readonly ILogger _logger; + private readonly CancellationTokenSource _shutdownCts; + private AsyncDuplexStreamingCall? _channel; - private readonly Channel<(Message Message, TaskCompletionSource WriteCompletionSource)> _outboundMessagesChannel = Channel.CreateBounded<(Message, TaskCompletionSource)>(new BoundedChannelOptions(1024) + public AutoRestartChannel(AgentRpc.AgentRpcClient client, + ILogger logger, + CancellationToken shutdownCancellation = default) { - AllowSynchronousContinuations = true, - SingleReader = true, - SingleWriter = false, - FullMode = BoundedChannelFullMode.Wait - }); + _client = client; + _logger = logger; + _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellation); + } - private readonly AgentRpc.AgentRpcClient _client = client; - public readonly IServiceProvider ServiceProvider = serviceProvider; + public void EnsureConnected() + { + _logger.LogInformation("Connecting to gRPC endpoint " + Environment.GetEnvironmentVariable("AGENT_HOST")); - private readonly ILogger _logger = logger; - private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); - private AsyncDuplexStreamingCall? _channel; - private Task? _readTask; - private Task? _writeTask; + if (this.RecreateChannel(null) == null) + { + throw new Exception("Failed to connect to gRPC endpoint."); + }; + } - private string _clientId = Guid.NewGuid().ToString(); - private CallOptions CallOptions + public AsyncDuplexStreamingCall StreamingCall { get { - var metadata = new Metadata + if (_channel is { } channel) { - { "client-id", this._clientId } - }; - return new CallOptions(headers: metadata); + return channel; + } + + lock (_channelLock) + { + if (_channel is not null) + { + return _channel; + } + + return RecreateChannel(null); + } } } - public IProtoSerializationRegistry SerializationRegistry { get; } = new ProtoSerializationRegistry(); + public AsyncDuplexStreamingCall RecreateChannel() => RecreateChannel(this._channel); + + private AsyncDuplexStreamingCall RecreateChannel(AsyncDuplexStreamingCall? ownedChannel) + { + // Make sure we are only re-creating the channel if it does not exit or we are the owner. + if (_channel is null || _channel == ownedChannel) + { + lock (_channelLock) + { + if (_channel is null || _channel == ownedChannel) + { + _channel?.Dispose(); + _channel = _client.OpenChannel(cancellationToken: _shutdownCts.Token); + } + } + } + + return _channel; + } public void Dispose() { - _outboundMessagesChannel.Writer.TryComplete(); - _channel?.Dispose(); + IDisposable? channelDisposable = Interlocked.Exchange(ref this._channel, null); + channelDisposable?.Dispose(); } +} + +internal sealed class MessageRouter(AgentRpc.AgentRpcClient client, + IMessageSink incomingMessageSink, + ILogger logger, + CancellationToken shutdownCancellation = default) : IDisposable +{ + private static readonly BoundedChannelOptions DefaultChannelOptions = new BoundedChannelOptions(1024) + { + AllowSynchronousContinuations = true, + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.Wait + }; + + private readonly ILogger _logger = logger; + + private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellation); + + private readonly IMessageSink _incomingMessageSink = incomingMessageSink; + private readonly Channel<(Message Message, TaskCompletionSource WriteCompletionSource)> _outboundMessagesChannel + // TODO: Enable a way to configure the channel options + = Channel.CreateBounded<(Message, TaskCompletionSource)>(DefaultChannelOptions); + + private readonly AutoRestartChannel _incomingMessageChannel = new AutoRestartChannel(client, logger, shutdownCancellation); + + private Task? _readTask; + private Task? _writeTask; private async Task RunReadPump() { - var channel = GetChannel(); + var cachedChannel = _incomingMessageChannel.StreamingCall; while (!_shutdownCts.Token.IsCancellationRequested) { try { - await foreach (var message in channel.ResponseStream.ReadAllAsync(_shutdownCts.Token)) + await foreach (var message in cachedChannel.ResponseStream.ReadAllAsync(_shutdownCts.Token)) { // next if message is null if (message == null) { continue; } - switch (message.MessageCase) - { - case Message.MessageOneofCase.Request: - var request = message.Request ?? throw new InvalidOperationException("Request is null."); - await HandleRequest(request); - break; - case Message.MessageOneofCase.Response: - var response = message.Response ?? throw new InvalidOperationException("Response is null."); - await HandleResponse(response); - break; - case Message.MessageOneofCase.CloudEvent: - var cloudEvent = message.CloudEvent ?? throw new InvalidOperationException("CloudEvent is null."); - await HandlePublish(cloudEvent); - break; - default: - throw new InvalidOperationException($"Unexpected message '{message}'."); - } + + await _incomingMessageSink.OnMessageAsync(message, _shutdownCts.Token); } } catch (OperationCanceledException) @@ -105,14 +146,199 @@ private async Task RunReadPump() catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) { _logger.LogError(ex, "Error reading from channel."); - channel = RecreateChannel(channel); + cachedChannel = this._incomingMessageChannel.RecreateChannel(); + } + catch + { + // Shutdown requested. + break; + } + } + } + + private async Task RunWritePump() + { + var cachedChannel = this._incomingMessageChannel.StreamingCall; + var outboundMessages = _outboundMessagesChannel.Reader; + while (!_shutdownCts.IsCancellationRequested) + { + (Message Message, TaskCompletionSource WriteCompletionSource) item = default; + try + { + await outboundMessages.WaitToReadAsync().ConfigureAwait(false); + + // Read the next message if we don't already have an unsent message + // waiting to be sent. + if (!outboundMessages.TryRead(out item)) + { + break; + } + + while (!_shutdownCts.IsCancellationRequested) + { + await cachedChannel.RequestStream.WriteAsync(item.Message, _shutdownCts.Token).ConfigureAwait(false); + item.WriteCompletionSource.TrySetResult(); + break; + } + } + catch (OperationCanceledException) + { + // Time to shut down. + item.WriteCompletionSource?.TrySetCanceled(); + break; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable) + { + // we could not connect to the endpoint - most likely we have the wrong port or failed ssl + // we need to let the user know what port we tried to connect to and then do backoff and retry + _logger.LogError(ex, "Error connecting to GRPC endpoint {Endpoint}.", Environment.GetEnvironmentVariable("AGENT_HOST")); + break; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.OK) + { + _logger.LogError(ex, "Error writing to channel, continuing (Status OK). {ex}", cachedChannel.ToString()); + break; + } + catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) + { + item.WriteCompletionSource?.TrySetException(ex); + _logger.LogError(ex, $"Error writing to channel.{ex}"); + cachedChannel = this._incomingMessageChannel.RecreateChannel(); + continue; } catch { // Shutdown requested. + item.WriteCompletionSource?.TrySetCanceled(); break; } } + + while (outboundMessages.TryRead(out var item)) + { + item.WriteCompletionSource.TrySetCanceled(); + } + } + + public ValueTask RouteMessageAsync(Message message, CancellationToken cancellation = default) + { + var tcs = new TaskCompletionSource(); + return _outboundMessagesChannel.Writer.WriteAsync((message, tcs), cancellation); + } + + public ValueTask StartAsync(CancellationToken cancellation) + { + // TODO: Should we error out on a noncancellable token? + + this._incomingMessageChannel.EnsureConnected(); + var didSuppress = false; + + // Make sure we do not mistakenly flow the ExecutionContext into the background pumping tasks. + if (!ExecutionContext.IsFlowSuppressed()) + { + didSuppress = true; + ExecutionContext.SuppressFlow(); + } + + try + { + _readTask = Task.Run(RunReadPump, cancellation); + _writeTask = Task.Run(RunWritePump, cancellation); + + return ValueTask.CompletedTask; + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + finally + { + if (didSuppress) + { + ExecutionContext.RestoreFlow(); + } + } + } + + // No point in returning a ValueTask here, since we are awaiting the two tasks + public async Task StopAsync() + { + _shutdownCts.Cancel(); + + _outboundMessagesChannel.Writer.TryComplete(); + + List pendingTasks = new(); + if (_readTask is { } readTask) + { + pendingTasks.Add(readTask); + } + + if (_writeTask is { } writeTask) + { + pendingTasks.Add(writeTask); + } + + await Task.WhenAll(pendingTasks).ConfigureAwait(false); + + this._incomingMessageChannel.Dispose(); + } + + public void Dispose() + { + _outboundMessagesChannel.Writer.TryComplete(); + this._incomingMessageChannel.Dispose(); + } +} + +public sealed class GrpcAgentRuntime: IHostedService, IAgentRuntime, IMessageSink, IDisposable +{ + public GrpcAgentRuntime(AgentRpc.AgentRpcClient client, + IHostApplicationLifetime hostApplicationLifetime, + IServiceProvider serviceProvider, + ILogger logger) + { + this._client = client; + this._logger = logger; + this._shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); + + this._messageRouter = new MessageRouter(client, this, logger, this._shutdownCts.Token); + + this.ServiceProvider = serviceProvider; + } + + // Request ID -> + private readonly ConcurrentDictionary> _pendingRequests = new(); + + private Dictionary>> agentFactories = new(); + private Dictionary agentInstances = new(); + + private readonly AgentRpc.AgentRpcClient _client; + private readonly MessageRouter _messageRouter; + + private readonly ILogger _logger; + private readonly CancellationTokenSource _shutdownCts; + + public IServiceProvider ServiceProvider { get; } + + private string _clientId = Guid.NewGuid().ToString(); + private CallOptions CallOptions + { + get + { + var metadata = new Metadata + { + { "client-id", this._clientId } + }; + return new CallOptions(headers: metadata); + } + } + + public IProtoSerializationRegistry SerializationRegistry { get; } = new ProtoSerializationRegistry(); + + public void Dispose() + { + this._shutdownCts.Cancel(); + this._messageRouter.Dispose(); } private async ValueTask HandleRequest(RpcRequest request, CancellationToken cancellationToken = default) @@ -163,7 +389,7 @@ private async ValueTask HandleRequest(RpcRequest request, CancellationToken canc Response = response }; - await WriteChannelAsync(responseMessage, cancellationToken); + await this._messageRouter.RouteMessageAsync(responseMessage, cancellationToken); } } @@ -227,69 +453,7 @@ private async ValueTask HandlePublish(CloudEvent evt, CancellationToken cancella await agent.OnMessageAsync(message, messageContext); } - private async Task RunWritePump() - { - var channel = GetChannel(); - var outboundMessages = _outboundMessagesChannel.Reader; - while (!_shutdownCts.IsCancellationRequested) - { - (Message Message, TaskCompletionSource WriteCompletionSource) item = default; - try - { - await outboundMessages.WaitToReadAsync().ConfigureAwait(false); - - // Read the next message if we don't already have an unsent message - // waiting to be sent. - if (!outboundMessages.TryRead(out item)) - { - break; - } - - while (!_shutdownCts.IsCancellationRequested) - { - await channel.RequestStream.WriteAsync(item.Message, _shutdownCts.Token).ConfigureAwait(false); - item.WriteCompletionSource.TrySetResult(); - break; - } - } - catch (OperationCanceledException) - { - // Time to shut down. - item.WriteCompletionSource?.TrySetCanceled(); - break; - } - catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable) - { - // we could not connect to the endpoint - most likely we have the wrong port or failed ssl - // we need to let the user know what port we tried to connect to and then do backoff and retry - _logger.LogError(ex, "Error connecting to GRPC endpoint {Endpoint}.", Environment.GetEnvironmentVariable("AGENT_HOST")); - break; - } - catch (RpcException ex) when (ex.StatusCode == StatusCode.OK) - { - _logger.LogError(ex, "Error writing to channel, continuing (Status OK). {ex}", channel.ToString()); - break; - } - catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) - { - item.WriteCompletionSource?.TrySetException(ex); - _logger.LogError(ex, $"Error writing to channel.{ex}"); - channel = RecreateChannel(channel); - continue; - } - catch - { - // Shutdown requested. - item.WriteCompletionSource?.TrySetCanceled(); - break; - } - } - - while (outboundMessages.TryRead(out var item)) - { - item.WriteCompletionSource.TrySetCanceled(); - } - } + // private override async ValueTask SendMessageAsync(Payload message, AgentId agentId, AgentId? agent = null, CancellationToken? cancellationToken = default) // { @@ -315,89 +479,17 @@ private async Task RunWritePump() // await WriteChannelAsync(new Message { Request = request }, cancellationToken).ConfigureAwait(false); // } - private async Task WriteChannelAsync(Message message, CancellationToken cancellationToken = default) + + public ValueTask StartAsync(CancellationToken cancellationToken) { - var tcs = new TaskCompletionSource(); - await _outboundMessagesChannel.Writer.WriteAsync((message, tcs), cancellationToken).ConfigureAwait(false); + return this._messageRouter.StartAsync(cancellationToken); } - private AsyncDuplexStreamingCall GetChannel() - { - if (_channel is { } channel) - { - return channel; - } - lock (_channelLock) - { - if (_channel is not null) - { - return _channel; - } + Task IHostedService.StartAsync(CancellationToken cancellationToken) => this._messageRouter.StartAsync(cancellationToken).AsTask(); - return RecreateChannel(null); - } - } - - private AsyncDuplexStreamingCall RecreateChannel(AsyncDuplexStreamingCall? channel) + public Task StopAsync(CancellationToken cancellationToken) { - if (_channel is null || _channel == channel) - { - lock (_channelLock) - { - if (_channel is null || _channel == channel) - { - _channel?.Dispose(); - _channel = _client.OpenChannel(cancellationToken: _shutdownCts.Token); - } - } - } - - return _channel; - } - public async Task StartAsync(CancellationToken cancellationToken) - { - _channel = GetChannel(); - _logger.LogInformation("Starting " + GetType().Name + ",connecting to gRPC endpoint " + Environment.GetEnvironmentVariable("AGENT_HOST")); - var didSuppress = false; - if (!ExecutionContext.IsFlowSuppressed()) - { - didSuppress = true; - ExecutionContext.SuppressFlow(); - } - - try - { - _readTask = Task.Run(RunReadPump, cancellationToken); - _writeTask = Task.Run(RunWritePump, cancellationToken); - } - finally - { - if (didSuppress) - { - ExecutionContext.RestoreFlow(); - } - } - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - _shutdownCts.Cancel(); - - _outboundMessagesChannel.Writer.TryComplete(); - - if (_readTask is { } readTask) - { - await readTask.ConfigureAwait(false); - } - - if (_writeTask is { } writeTask) - { - await writeTask.ConfigureAwait(false); - } - lock (_channelLock) - { - _channel?.Dispose(); - } + return this._messageRouter.StopAsync(); } private async ValueTask EnsureAgentAsync(Contracts.AgentId agentId) @@ -466,10 +558,11 @@ private object PayloadToObject(Payload payload) { { Request = request }; + // Create a future that will be completed when the response is received var resultSink = new ResultSink(); this._pendingRequests.TryAdd(request.RequestId, resultSink); - await WriteChannelAsync(msg, cancellationToken); + await this._messageRouter.RouteMessageAsync(msg, cancellationToken); return await resultSink.Future; } @@ -518,7 +611,8 @@ public async ValueTask PublishMessageAsync(object message, TopicId topic, Contra { CloudEvent = cloudEvent }; - await WriteChannelAsync(msg, cancellationToken); + + await this._messageRouter.RouteMessageAsync(msg, cancellationToken); } public ValueTask GetAgentAsync(Contracts.AgentId agentId, bool lazy = true) @@ -593,5 +687,26 @@ public ValueTask LoadStateAsync(IDictionary state) { throw new NotImplementedException(); } + + public async ValueTask OnMessageAsync(Message message, CancellationToken cancellation = default) + { + switch (message.MessageCase) + { + case Message.MessageOneofCase.Request: + var request = message.Request ?? throw new InvalidOperationException("Request is null."); + await HandleRequest(request); + break; + case Message.MessageOneofCase.Response: + var response = message.Response ?? throw new InvalidOperationException("Response is null."); + await HandleResponse(response); + break; + case Message.MessageOneofCase.CloudEvent: + var cloudEvent = message.CloudEvent ?? throw new InvalidOperationException("CloudEvent is null."); + await HandlePublish(cloudEvent); + break; + default: + throw new InvalidOperationException($"Unexpected message '{message}'."); + } + } } From df3ca7234318d11b6b1c5279e7abe12e5165fdc4 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Wed, 29 Jan 2025 23:05:20 -0500 Subject: [PATCH 03/25] refactor: Move GrpcMessageRouter to own file --- .../Core.Grpc/GrpcAgentRuntime.cs | 281 +---------------- .../Core.Grpc/GrpcMessageRouter.cs | 288 ++++++++++++++++++ 2 files changed, 290 insertions(+), 279 deletions(-) create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs index 1797182a9704..b1f5d43669e6 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -2,7 +2,6 @@ // GrpcAgentRuntime.cs using System.Collections.Concurrent; -using System.Threading.Channels; using Google.Protobuf; using Grpc.Core; using Microsoft.AutoGen.Contracts; @@ -12,283 +11,7 @@ namespace Microsoft.AutoGen.Core.Grpc; -// TODO: Consider whether we want to just reuse IHandle -internal interface IMessageSink -{ - public ValueTask OnMessageAsync(TMessage message, CancellationToken cancellation = default); -} - -internal sealed class AutoRestartChannel : IDisposable -{ - private readonly object _channelLock = new(); - private readonly AgentRpc.AgentRpcClient _client; - private readonly ILogger _logger; - private readonly CancellationTokenSource _shutdownCts; - private AsyncDuplexStreamingCall? _channel; - - public AutoRestartChannel(AgentRpc.AgentRpcClient client, - ILogger logger, - CancellationToken shutdownCancellation = default) - { - _client = client; - _logger = logger; - _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellation); - } - - public void EnsureConnected() - { - _logger.LogInformation("Connecting to gRPC endpoint " + Environment.GetEnvironmentVariable("AGENT_HOST")); - - if (this.RecreateChannel(null) == null) - { - throw new Exception("Failed to connect to gRPC endpoint."); - }; - } - - public AsyncDuplexStreamingCall StreamingCall - { - get - { - if (_channel is { } channel) - { - return channel; - } - - lock (_channelLock) - { - if (_channel is not null) - { - return _channel; - } - - return RecreateChannel(null); - } - } - } - - public AsyncDuplexStreamingCall RecreateChannel() => RecreateChannel(this._channel); - - private AsyncDuplexStreamingCall RecreateChannel(AsyncDuplexStreamingCall? ownedChannel) - { - // Make sure we are only re-creating the channel if it does not exit or we are the owner. - if (_channel is null || _channel == ownedChannel) - { - lock (_channelLock) - { - if (_channel is null || _channel == ownedChannel) - { - _channel?.Dispose(); - _channel = _client.OpenChannel(cancellationToken: _shutdownCts.Token); - } - } - } - - return _channel; - } - - public void Dispose() - { - IDisposable? channelDisposable = Interlocked.Exchange(ref this._channel, null); - channelDisposable?.Dispose(); - } -} - -internal sealed class MessageRouter(AgentRpc.AgentRpcClient client, - IMessageSink incomingMessageSink, - ILogger logger, - CancellationToken shutdownCancellation = default) : IDisposable -{ - private static readonly BoundedChannelOptions DefaultChannelOptions = new BoundedChannelOptions(1024) - { - AllowSynchronousContinuations = true, - SingleReader = true, - SingleWriter = false, - FullMode = BoundedChannelFullMode.Wait - }; - - private readonly ILogger _logger = logger; - - private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellation); - - private readonly IMessageSink _incomingMessageSink = incomingMessageSink; - private readonly Channel<(Message Message, TaskCompletionSource WriteCompletionSource)> _outboundMessagesChannel - // TODO: Enable a way to configure the channel options - = Channel.CreateBounded<(Message, TaskCompletionSource)>(DefaultChannelOptions); - - private readonly AutoRestartChannel _incomingMessageChannel = new AutoRestartChannel(client, logger, shutdownCancellation); - - private Task? _readTask; - private Task? _writeTask; - - private async Task RunReadPump() - { - var cachedChannel = _incomingMessageChannel.StreamingCall; - while (!_shutdownCts.Token.IsCancellationRequested) - { - try - { - await foreach (var message in cachedChannel.ResponseStream.ReadAllAsync(_shutdownCts.Token)) - { - // next if message is null - if (message == null) - { - continue; - } - - await _incomingMessageSink.OnMessageAsync(message, _shutdownCts.Token); - } - } - catch (OperationCanceledException) - { - // Time to shut down. - break; - } - catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) - { - _logger.LogError(ex, "Error reading from channel."); - cachedChannel = this._incomingMessageChannel.RecreateChannel(); - } - catch - { - // Shutdown requested. - break; - } - } - } - - private async Task RunWritePump() - { - var cachedChannel = this._incomingMessageChannel.StreamingCall; - var outboundMessages = _outboundMessagesChannel.Reader; - while (!_shutdownCts.IsCancellationRequested) - { - (Message Message, TaskCompletionSource WriteCompletionSource) item = default; - try - { - await outboundMessages.WaitToReadAsync().ConfigureAwait(false); - - // Read the next message if we don't already have an unsent message - // waiting to be sent. - if (!outboundMessages.TryRead(out item)) - { - break; - } - while (!_shutdownCts.IsCancellationRequested) - { - await cachedChannel.RequestStream.WriteAsync(item.Message, _shutdownCts.Token).ConfigureAwait(false); - item.WriteCompletionSource.TrySetResult(); - break; - } - } - catch (OperationCanceledException) - { - // Time to shut down. - item.WriteCompletionSource?.TrySetCanceled(); - break; - } - catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable) - { - // we could not connect to the endpoint - most likely we have the wrong port or failed ssl - // we need to let the user know what port we tried to connect to and then do backoff and retry - _logger.LogError(ex, "Error connecting to GRPC endpoint {Endpoint}.", Environment.GetEnvironmentVariable("AGENT_HOST")); - break; - } - catch (RpcException ex) when (ex.StatusCode == StatusCode.OK) - { - _logger.LogError(ex, "Error writing to channel, continuing (Status OK). {ex}", cachedChannel.ToString()); - break; - } - catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) - { - item.WriteCompletionSource?.TrySetException(ex); - _logger.LogError(ex, $"Error writing to channel.{ex}"); - cachedChannel = this._incomingMessageChannel.RecreateChannel(); - continue; - } - catch - { - // Shutdown requested. - item.WriteCompletionSource?.TrySetCanceled(); - break; - } - } - - while (outboundMessages.TryRead(out var item)) - { - item.WriteCompletionSource.TrySetCanceled(); - } - } - - public ValueTask RouteMessageAsync(Message message, CancellationToken cancellation = default) - { - var tcs = new TaskCompletionSource(); - return _outboundMessagesChannel.Writer.WriteAsync((message, tcs), cancellation); - } - - public ValueTask StartAsync(CancellationToken cancellation) - { - // TODO: Should we error out on a noncancellable token? - - this._incomingMessageChannel.EnsureConnected(); - var didSuppress = false; - - // Make sure we do not mistakenly flow the ExecutionContext into the background pumping tasks. - if (!ExecutionContext.IsFlowSuppressed()) - { - didSuppress = true; - ExecutionContext.SuppressFlow(); - } - - try - { - _readTask = Task.Run(RunReadPump, cancellation); - _writeTask = Task.Run(RunWritePump, cancellation); - - return ValueTask.CompletedTask; - } - catch (Exception ex) - { - return ValueTask.FromException(ex); - } - finally - { - if (didSuppress) - { - ExecutionContext.RestoreFlow(); - } - } - } - - // No point in returning a ValueTask here, since we are awaiting the two tasks - public async Task StopAsync() - { - _shutdownCts.Cancel(); - - _outboundMessagesChannel.Writer.TryComplete(); - - List pendingTasks = new(); - if (_readTask is { } readTask) - { - pendingTasks.Add(readTask); - } - - if (_writeTask is { } writeTask) - { - pendingTasks.Add(writeTask); - } - - await Task.WhenAll(pendingTasks).ConfigureAwait(false); - - this._incomingMessageChannel.Dispose(); - } - - public void Dispose() - { - _outboundMessagesChannel.Writer.TryComplete(); - this._incomingMessageChannel.Dispose(); - } -} public sealed class GrpcAgentRuntime: IHostedService, IAgentRuntime, IMessageSink, IDisposable { @@ -301,7 +24,7 @@ public GrpcAgentRuntime(AgentRpc.AgentRpcClient client, this._logger = logger; this._shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); - this._messageRouter = new MessageRouter(client, this, logger, this._shutdownCts.Token); + this._messageRouter = new GrpcMessageRouter(client, this, logger, this._shutdownCts.Token); this.ServiceProvider = serviceProvider; } @@ -313,7 +36,7 @@ public GrpcAgentRuntime(AgentRpc.AgentRpcClient client, private Dictionary agentInstances = new(); private readonly AgentRpc.AgentRpcClient _client; - private readonly MessageRouter _messageRouter; + private readonly GrpcMessageRouter _messageRouter; private readonly ILogger _logger; private readonly CancellationTokenSource _shutdownCts; diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs new file mode 100644 index 000000000000..0ecf8ee1413e --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GrpcMessageRouter.cs + +using System.Threading.Channels; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.AutoGen.Protobuf; + +namespace Microsoft.AutoGen.Core.Grpc; + +// TODO: Consider whether we want to just reuse IHandle +internal interface IMessageSink +{ + public ValueTask OnMessageAsync(TMessage message, CancellationToken cancellation = default); +} + +internal sealed class AutoRestartChannel : IDisposable +{ + private readonly object _channelLock = new(); + private readonly AgentRpc.AgentRpcClient _client; + private readonly ILogger _logger; + private readonly CancellationTokenSource _shutdownCts; + private AsyncDuplexStreamingCall? _channel; + + public AutoRestartChannel(AgentRpc.AgentRpcClient client, + ILogger logger, + CancellationToken shutdownCancellation = default) + { + _client = client; + _logger = logger; + _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellation); + } + + public void EnsureConnected() + { + _logger.LogInformation("Connecting to gRPC endpoint " + Environment.GetEnvironmentVariable("AGENT_HOST")); + + if (this.RecreateChannel(null) == null) + { + throw new Exception("Failed to connect to gRPC endpoint."); + }; + } + + public AsyncDuplexStreamingCall StreamingCall + { + get + { + if (_channel is { } channel) + { + return channel; + } + + lock (_channelLock) + { + if (_channel is not null) + { + return _channel; + } + + return RecreateChannel(null); + } + } + } + + public AsyncDuplexStreamingCall RecreateChannel() => RecreateChannel(this._channel); + + private AsyncDuplexStreamingCall RecreateChannel(AsyncDuplexStreamingCall? ownedChannel) + { + // Make sure we are only re-creating the channel if it does not exit or we are the owner. + if (_channel is null || _channel == ownedChannel) + { + lock (_channelLock) + { + if (_channel is null || _channel == ownedChannel) + { + _channel?.Dispose(); + _channel = _client.OpenChannel(cancellationToken: _shutdownCts.Token); + } + } + } + + return _channel; + } + + public void Dispose() + { + IDisposable? channelDisposable = Interlocked.Exchange(ref this._channel, null); + channelDisposable?.Dispose(); + } +} + +internal sealed class GrpcMessageRouter(AgentRpc.AgentRpcClient client, + IMessageSink incomingMessageSink, + ILogger logger, + CancellationToken shutdownCancellation = default) : IDisposable +{ + private static readonly BoundedChannelOptions DefaultChannelOptions = new BoundedChannelOptions(1024) + { + AllowSynchronousContinuations = true, + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.Wait + }; + + private readonly ILogger _logger = logger; + + private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellation); + + private readonly IMessageSink _incomingMessageSink = incomingMessageSink; + private readonly Channel<(Message Message, TaskCompletionSource WriteCompletionSource)> _outboundMessagesChannel + // TODO: Enable a way to configure the channel options + = Channel.CreateBounded<(Message, TaskCompletionSource)>(DefaultChannelOptions); + + private readonly AutoRestartChannel _incomingMessageChannel = new AutoRestartChannel(client, logger, shutdownCancellation); + + private Task? _readTask; + private Task? _writeTask; + + private async Task RunReadPump() + { + var cachedChannel = _incomingMessageChannel.StreamingCall; + while (!_shutdownCts.Token.IsCancellationRequested) + { + try + { + await foreach (var message in cachedChannel.ResponseStream.ReadAllAsync(_shutdownCts.Token)) + { + // next if message is null + if (message == null) + { + continue; + } + + await _incomingMessageSink.OnMessageAsync(message, _shutdownCts.Token); + } + } + catch (OperationCanceledException) + { + // Time to shut down. + break; + } + catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) + { + _logger.LogError(ex, "Error reading from channel."); + cachedChannel = this._incomingMessageChannel.RecreateChannel(); + } + catch + { + // Shutdown requested. + break; + } + } + } + + private async Task RunWritePump() + { + var cachedChannel = this._incomingMessageChannel.StreamingCall; + var outboundMessages = _outboundMessagesChannel.Reader; + while (!_shutdownCts.IsCancellationRequested) + { + (Message Message, TaskCompletionSource WriteCompletionSource) item = default; + try + { + await outboundMessages.WaitToReadAsync().ConfigureAwait(false); + + // Read the next message if we don't already have an unsent message + // waiting to be sent. + if (!outboundMessages.TryRead(out item)) + { + break; + } + + while (!_shutdownCts.IsCancellationRequested) + { + await cachedChannel.RequestStream.WriteAsync(item.Message, _shutdownCts.Token).ConfigureAwait(false); + item.WriteCompletionSource.TrySetResult(); + break; + } + } + catch (OperationCanceledException) + { + // Time to shut down. + item.WriteCompletionSource?.TrySetCanceled(); + break; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable) + { + // we could not connect to the endpoint - most likely we have the wrong port or failed ssl + // we need to let the user know what port we tried to connect to and then do backoff and retry + _logger.LogError(ex, "Error connecting to GRPC endpoint {Endpoint}.", Environment.GetEnvironmentVariable("AGENT_HOST")); + break; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.OK) + { + _logger.LogError(ex, "Error writing to channel, continuing (Status OK). {ex}", cachedChannel.ToString()); + break; + } + catch (Exception ex) when (!_shutdownCts.IsCancellationRequested) + { + item.WriteCompletionSource?.TrySetException(ex); + _logger.LogError(ex, $"Error writing to channel.{ex}"); + cachedChannel = this._incomingMessageChannel.RecreateChannel(); + continue; + } + catch + { + // Shutdown requested. + item.WriteCompletionSource?.TrySetCanceled(); + break; + } + } + + while (outboundMessages.TryRead(out var item)) + { + item.WriteCompletionSource.TrySetCanceled(); + } + } + + public ValueTask RouteMessageAsync(Message message, CancellationToken cancellation = default) + { + var tcs = new TaskCompletionSource(); + return _outboundMessagesChannel.Writer.WriteAsync((message, tcs), cancellation); + } + + public ValueTask StartAsync(CancellationToken cancellation) + { + // TODO: Should we error out on a noncancellable token? + + this._incomingMessageChannel.EnsureConnected(); + var didSuppress = false; + + // Make sure we do not mistakenly flow the ExecutionContext into the background pumping tasks. + if (!ExecutionContext.IsFlowSuppressed()) + { + didSuppress = true; + ExecutionContext.SuppressFlow(); + } + + try + { + _readTask = Task.Run(RunReadPump, cancellation); + _writeTask = Task.Run(RunWritePump, cancellation); + + return ValueTask.CompletedTask; + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + finally + { + if (didSuppress) + { + ExecutionContext.RestoreFlow(); + } + } + } + + // No point in returning a ValueTask here, since we are awaiting the two tasks + public async Task StopAsync() + { + _shutdownCts.Cancel(); + + _outboundMessagesChannel.Writer.TryComplete(); + + List pendingTasks = new(); + if (_readTask is { } readTask) + { + pendingTasks.Add(readTask); + } + + if (_writeTask is { } writeTask) + { + pendingTasks.Add(writeTask); + } + + await Task.WhenAll(pendingTasks).ConfigureAwait(false); + + this._incomingMessageChannel.Dispose(); + } + + public void Dispose() + { + _outboundMessagesChannel.Writer.TryComplete(); + this._incomingMessageChannel.Dispose(); + } +} + From 9d981dcca9673dbe3c954ec5e73779623e1e29cc Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Wed, 29 Jan 2025 23:51:03 -0500 Subject: [PATCH 04/25] refactor: Add default IAgentRuntime.GetAgent implementations --- dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs | 6 ++++-- dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs | 6 ------ dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs index 0d84fbe72d37..bb360617dc09 100644 --- a/dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs @@ -53,7 +53,8 @@ public interface IAgentRuntime : ISaveState /// An optional key to specify variations of the agent. Defaults to "default". /// If true, the agent is fetched lazily. /// A task representing the asynchronous operation, returning the agent's ID. - public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/); + public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/) + => this.GetAgentAsync(new AgentId(agentType, key), lazy); /// /// Retrieves an agent by its string representation. @@ -62,7 +63,8 @@ public interface IAgentRuntime : ISaveState /// An optional key to specify variations of the agent. Defaults to "default". /// If true, the agent is fetched lazily. /// A task representing the asynchronous operation, returning the agent's ID. - public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/); + public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/) + => this.GetAgentAsync(new AgentId(agent, key), lazy); /// /// Saves the state of an agent. diff --git a/dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs index 69b2d314e550..791376ae56e8 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs @@ -140,12 +140,6 @@ public async ValueTask GetAgentAsync(AgentId agentId, bool lazy = true) return agentId; } - public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true) - => this.GetAgentAsync(new AgentId(agentType, key), lazy); - - public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true) - => this.GetAgentAsync(new AgentId(agent, key), lazy); - public async ValueTask GetAgentMetadataAsync(AgentId agentId) { IHostableAgent agent = await this.EnsureAgentAsync(agentId); diff --git a/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs index cc39b3564c66..da81f40a6f3c 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs @@ -121,7 +121,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => }); Assert.Null(agent); - await runtime.GetAgentAsync("MyAgent", lazy: false); + await runtime.GetAgentAsync(AgentId.FromStr("MyAgent"), lazy: false); Assert.NotNull(agent); Assert.True(agent.ReceivedItems.Count == 0); From edf64ba72fb25931318e62c44ac3429199c0762f Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Wed, 29 Jan 2025 23:54:33 -0500 Subject: [PATCH 05/25] feat: Implement remaining methods in GrpcAgentRuntime * Factor out AgentContainer for managing factory registration, instantiation, and subscription management --- .../Core.Grpc/GrpcAgentRuntime.cs | 227 +++++++++++------- 1 file changed, 136 insertions(+), 91 deletions(-) diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs index b1f5d43669e6..f0be2376a153 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -11,7 +11,74 @@ namespace Microsoft.AutoGen.Core.Grpc; +internal sealed class AgentsContainer(IAgentRuntime hostingRuntime) +{ + private readonly IAgentRuntime hostingRuntime = hostingRuntime; + + private Dictionary agentInstances = new(); + private Dictionary subscriptions = new(); + private Dictionary>> agentFactories = new(); + + public async ValueTask EnsureAgentAsync(Contracts.AgentId agentId) + { + if (!this.agentInstances.TryGetValue(agentId, out IHostableAgent? agent)) + { + if (!this.agentFactories.TryGetValue(agentId.Type, out Func>? factoryFunc)) + { + throw new Exception($"Agent with name {agentId.Type} not found."); + } + + agent = await factoryFunc(agentId, this.hostingRuntime); + this.agentInstances.Add(agentId, agent); + } + + return this.agentInstances[agentId]; + } + + public async ValueTask GetAgentAsync(Contracts.AgentId agentId, bool lazy = true) + { + if (!lazy) + { + await this.EnsureAgentAsync(agentId); + } + + return agentId; + } + + public AgentType RegisterAgentFactory(AgentType type, Func> factoryFunc) + { + if (this.agentFactories.ContainsKey(type)) + { + throw new Exception($"Agent factory with type {type} already exists."); + } + this.agentFactories.Add(type, factoryFunc); + return type; + } + + public void AddSubscription(ISubscriptionDefinition subscription) + { + if (this.subscriptions.ContainsKey(subscription.Id)) + { + throw new Exception($"Subscription with id {subscription.Id} already exists."); + } + + this.subscriptions.Add(subscription.Id, subscription); + } + + public bool RemoveSubscriptionAsync(string subscriptionId) + { + if (!this.subscriptions.ContainsKey(subscriptionId)) + { + throw new Exception($"Subscription with id {subscriptionId} does not exist."); + } + + return this.subscriptions.Remove(subscriptionId); + } + + public HashSet RegisteredAgentTypes => this.agentFactories.Keys.ToHashSet(); + public IEnumerable LiveAgents => this.agentInstances.Values; +} public sealed class GrpcAgentRuntime: IHostedService, IAgentRuntime, IMessageSink, IDisposable { @@ -25,21 +92,21 @@ public GrpcAgentRuntime(AgentRpc.AgentRpcClient client, this._shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); this._messageRouter = new GrpcMessageRouter(client, this, logger, this._shutdownCts.Token); + this._agentsContainer = new AgentsContainer(this); this.ServiceProvider = serviceProvider; } - // Request ID -> + // Request ID -> ResultSink<...> private readonly ConcurrentDictionary> _pendingRequests = new(); - private Dictionary>> agentFactories = new(); - private Dictionary agentInstances = new(); - private readonly AgentRpc.AgentRpcClient _client; private readonly GrpcMessageRouter _messageRouter; private readonly ILogger _logger; private readonly CancellationTokenSource _shutdownCts; + + private readonly AgentsContainer _agentsContainer; public IServiceProvider ServiceProvider { get; } @@ -84,7 +151,7 @@ private async ValueTask HandleRequest(RpcRequest request, CancellationToken canc } var agentId = request.Target; - var agent = await EnsureAgentAsync(agentId.FromProtobuf()); + var agent = await this._agentsContainer.EnsureAgentAsync(agentId.FromProtobuf()); // Convert payload back to object var payload = request.Payload; @@ -172,36 +239,9 @@ private async ValueTask HandlePublish(CloudEvent evt, CancellationToken cancella Topic = topic, IsRpc = false }; - var agent = await EnsureAgentAsync(sender); + var agent = await this._agentsContainer.EnsureAgentAsync(sender); await agent.OnMessageAsync(message, messageContext); } - - - - // private override async ValueTask SendMessageAsync(Payload message, AgentId agentId, AgentId? agent = null, CancellationToken? cancellationToken = default) - // { - // var request = new RpcRequest - // { - // RequestId = Guid.NewGuid().ToString(), - // Source = agent, - // Target = agentId, - // Payload = message, - // }; - - // // Actually send it and wait for the response - // throw new NotImplementedException(); - // } - - // new is intentional - - // public new async ValueTask RuntimeSendRequestAsync(IAgent agent, RpcRequest request, CancellationToken cancellationToken = default) - // { - // var requestId = Guid.NewGuid().ToString(); - // _pendingRequests[requestId] = ((Agent)agent, request.RequestId); - // request.RequestId = requestId; - // await WriteChannelAsync(new Message { Request = request }, cancellationToken).ConfigureAwait(false); - // } - public ValueTask StartAsync(CancellationToken cancellationToken) { @@ -215,22 +255,6 @@ public Task StopAsync(CancellationToken cancellationToken) return this._messageRouter.StopAsync(); } - private async ValueTask EnsureAgentAsync(Contracts.AgentId agentId) - { - if (!this.agentInstances.TryGetValue(agentId, out IHostableAgent? agent)) - { - if (!this.agentFactories.TryGetValue(agentId.Type, out Func>? factoryFunc)) - { - throw new Exception($"Agent with name {agentId.Type} not found."); - } - - agent = await factoryFunc(agentId, this); - this.agentInstances.Add(agentId, agent); - } - - return this.agentInstances[agentId]; - } - private Payload ObjectToPayload(object message) { if (!SerializationRegistry.Exists(message.GetType())) { @@ -338,77 +362,98 @@ public async ValueTask PublishMessageAsync(object message, TopicId topic, Contra await this._messageRouter.RouteMessageAsync(msg, cancellationToken); } - public ValueTask GetAgentAsync(Contracts.AgentId agentId, bool lazy = true) - { - throw new NotImplementedException(); - } + public ValueTask GetAgentAsync(Contracts.AgentId agentId, bool lazy = true) => this._agentsContainer.GetAgentAsync(agentId, lazy); - public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true) + public async ValueTask> SaveAgentStateAsync(Contracts.AgentId agentId) { - throw new NotImplementedException(); + IHostableAgent agent = await this._agentsContainer.EnsureAgentAsync(agentId); + return await agent.SaveStateAsync(); } - public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true) + public async ValueTask LoadAgentStateAsync(Contracts.AgentId agentId, IDictionary state) { - throw new NotImplementedException(); + IHostableAgent agent = await this._agentsContainer.EnsureAgentAsync(agentId); + await agent.LoadStateAsync(state); } - public ValueTask> SaveAgentStateAsync(Contracts.AgentId agentId) + public async ValueTask GetAgentMetadataAsync(Contracts.AgentId agentId) { - throw new NotImplementedException(); + IHostableAgent agent = await this._agentsContainer.EnsureAgentAsync(agentId); + return agent.Metadata; } - public ValueTask LoadAgentStateAsync(Contracts.AgentId agentId, IDictionary state) + public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription) { - throw new NotImplementedException(); + this._agentsContainer.AddSubscription(subscription); + + // Because we have an extensible definition of ISubscriptionDefinition, we cannot project it to the Gateway. + // What this means is that we will have a much chattier interface between the Gateway and the Runtime. + + //await this._client.AddSubscriptionAsync(new AddSubscriptionRequest + //{ + // Subscription = new Subscription + // { + // Id = subscription.Id, + // TopicType = subscription.TopicType, + // AgentType = subscription.AgentType.Name + // } + //}, this.CallOptions); + + return ValueTask.CompletedTask; } - public ValueTask GetAgentMetadataAsync(Contracts.AgentId agentId) + public ValueTask RemoveSubscriptionAsync(string subscriptionId) { - throw new NotImplementedException(); - } + this._agentsContainer.RemoveSubscriptionAsync(subscriptionId); - public async ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription) - { - var _ = await this._client.AddSubscriptionAsync(new AddSubscriptionRequest{ - Subscription = subscription.ToProtobuf() - },this.CallOptions); + // See above (AddSubscriptionAsync) for why this is commented out. + + //await this._client.RemoveSubscriptionAsync(new RemoveSubscriptionRequest + //{ + // Id = subscriptionId + //}, this.CallOptions); + + return ValueTask.CompletedTask; } - public ValueTask RemoveSubscriptionAsync(string subscriptionId) + public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) + => ValueTask.FromResult(this._agentsContainer.RegisterAgentFactory(type, factoryFunc)); + + public ValueTask TryGetAgentProxyAsync(Contracts.AgentId agentId) { - throw new NotImplementedException(); + // TODO: Do we want to support getting remote agent proxies? + return ValueTask.FromResult(new AgentProxy(agentId, this)); } - public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) + public async ValueTask> SaveStateAsync() { - if (this.agentFactories.ContainsKey(type)) + Dictionary state = new(); + foreach (var agent in this._agentsContainer.LiveAgents) { - throw new Exception($"Agent with type {type} already exists."); + state[agent.Id.ToString()] = await agent.SaveStateAsync(); } - this.agentFactories.Add(type, async (agentId, runtime) => await factoryFunc(agentId, runtime)); - - this._client.RegisterAgentAsync(new RegisterAgentTypeRequest - { - Type = type.Name, - }, this.CallOptions); - return ValueTask.FromResult(type); + return state; } - public ValueTask TryGetAgentProxyAsync(Contracts.AgentId agentId) + public async ValueTask LoadStateAsync(IDictionary state) { - throw new NotImplementedException(); - } + HashSet registeredTypes = this._agentsContainer.RegisteredAgentTypes; - public ValueTask> SaveStateAsync() - { - throw new NotImplementedException(); - } + foreach (var agentIdStr in state.Keys) + { + Contracts.AgentId agentId = Contracts.AgentId.FromStr(agentIdStr); + if (state[agentIdStr] is not IDictionary agentStateDict) + { + throw new Exception($"Agent state for {agentId} is not a {typeof(IDictionary)}: {state[agentIdStr].GetType()}"); + } - public ValueTask LoadStateAsync(IDictionary state) - { - throw new NotImplementedException(); + if (registeredTypes.Contains(agentId.Type)) + { + IHostableAgent agent = await this._agentsContainer.EnsureAgentAsync(agentId); + await agent.LoadStateAsync(agentStateDict); + } + } } public async ValueTask OnMessageAsync(Message message, CancellationToken cancellation = default) From 1369a5d078a7f01090fb46a5473faed1b7084c49 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 30 Jan 2025 00:19:29 -0500 Subject: [PATCH 06/25] fix: Get Core.Grpc test project building --- .../Core.Grpc/AgentsAppBuilderExtensions.cs | 21 ++ .../AgentGrpcTests.cs | 346 ++++++++++-------- .../Microsoft.AutoGen.Core.Grpc.Tests.csproj | 2 +- 3 files changed, 212 insertions(+), 157 deletions(-) create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs new file mode 100644 index 000000000000..e19cc2f343d2 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentsAppBuilderExtensions.cs + +using Microsoft.AutoGen.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AutoGen.Core.Grpc; + +public static class AgentsAppBuilderExtensions +{ + public static AgentsAppBuilder UseGrpcRuntime(this AgentsAppBuilder this_, bool deliverToSelf = false) + { + this_.Services.AddSingleton(); + this_.Services.AddHostedService(services => + { + return (services.GetRequiredService() as GrpcAgentRuntime)!; + }); + + return this_; + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs index 7514609e145b..bb054ee738a4 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -1,184 +1,214 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // AgentGrpcTests.cs -using System.Collections.Concurrent; -using System.Text.Json; -using FluentAssertions; -using Google.Protobuf.Reflection; +//using System.Collections.Concurrent; +//using System.Text.Json; +//using FluentAssertions; +//using Google.Protobuf.Reflection; using Microsoft.AutoGen.Contracts; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit; -using static Microsoft.AutoGen.Core.Grpc.Tests.AgentGrpcTests; namespace Microsoft.AutoGen.Core.Grpc.Tests; [Trait("Category", "UnitV2")] public class AgentGrpcTests { - /// - /// Verify that if the agent is not initialized via AgentWorker, it should throw the correct exception. - /// - /// void [Fact] - public async Task Agent_ShouldThrowException_WhenNotInitialized() - { - using var runtime = new GrpcRuntime(); - var (_, agent) = runtime.Start(false); // Do not initialize - - // Expect an exception when calling AddSubscriptionAsync because the agent is uninitialized - await Assert.ThrowsAsync( - async () => await agent.AddSubscriptionAsync("TestEvent") - ); - } - - /// - /// validate that the agent is initialized correctly with implicit subs - /// - /// void - [Fact] - public async Task Agent_ShouldInitializeCorrectly() + public void Agent_ShouldInitializeCorrectly() { using var runtime = new GrpcRuntime(); var (worker, agent) = runtime.Start(); Assert.Equal(nameof(GrpcAgentRuntime), worker.GetType().Name); - await Task.Delay(5000); - var subscriptions = await agent.GetSubscriptionsAsync(); - Assert.Equal(2, subscriptions.Count); - } - /// - /// Test AddSubscriptionAsync method - /// - /// void - [Fact] - public async Task SubscribeAsync_UnsubscribeAsync_and_GetSubscriptionsTest() - { - using var runtime = new GrpcRuntime(); - var (_, agent) = runtime.Start(); - await agent.AddSubscriptionAsync("TestEvent"); - await Task.Delay(100); - var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - var found = false; - foreach (var subscription in subscriptions) - { - if (subscription.TypeSubscription.TopicType == "TestEvent") - { - found = true; - } - } - Assert.True(found); - await agent.RemoveSubscriptionAsync("TestEvent").ConfigureAwait(true); - await Task.Delay(1000); - subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - found = false; - foreach (var subscription in subscriptions) - { - if (subscription.TypeSubscription.TopicType == "TestEvent") - { - found = true; - } - } - Assert.False(found); } - /// - /// Test StoreAsync and ReadAsync methods - /// - /// void - [Fact] - public async Task StoreAsync_and_ReadAsyncTest() - { - using var runtime = new GrpcRuntime(); - var (_, agent) = runtime.Start(); - Dictionary state = new() - { - { "testdata", "Active" } - }; - await agent.StoreAsync(new AgentState - { - AgentId = agent.AgentId, - TextData = JsonSerializer.Serialize(state) - }).ConfigureAwait(true); - var readState = await agent.ReadAsync(agent.AgentId).ConfigureAwait(true); - var read = JsonSerializer.Deserialize>(readState.TextData) ?? new Dictionary { { "data", "No state data found" } }; - read.TryGetValue("testdata", out var value); - Assert.Equal("Active", value); - } + ///// + ///// Verify that if the agent is not initialized via AgentWorker, it should throw the correct exception. + ///// + ///// void + //[Fact] + //public async Task Agent_ShouldThrowException_WhenNotInitialized() + //{ + // using var runtime = new GrpcRuntime(); + // var (_, agent) = runtime.Start(false); // Do not initialize - /// - /// Test PublishMessageAsync method and ReceiveMessage method - /// - /// void - [Fact] - public async Task PublishMessageAsync_and_ReceiveMessageTest() + // // Expect an exception when calling AddSubscriptionAsync because the agent is uninitialized + // await Assert.ThrowsAsync( + // async () => await agent.AddSubscriptionAsync("TestEvent") + // ); + //} + + ///// + ///// validate that the agent is initialized correctly with implicit subs + ///// + ///// void + //[Fact] + //public async Task Agent_ShouldInitializeCorrectly() + //{ + // using var runtime = new GrpcRuntime(); + // var (worker, agent) = runtime.Start(); + // Assert.Equal(nameof(GrpcAgentRuntime), worker.GetType().Name); + // await Task.Delay(5000); + // var subscriptions = await agent.GetSubscriptionsAsync(); + // Assert.Equal(2, subscriptions.Count); + //} + ///// + ///// Test AddSubscriptionAsync method + ///// + ///// void + //[Fact] + //public async Task SubscribeAsync_UnsubscribeAsync_and_GetSubscriptionsTest() + //{ + // using var runtime = new GrpcRuntime(); + // var (_, agent) = runtime.Start(); + // await agent.AddSubscriptionAsync("TestEvent"); + // await Task.Delay(100); + // var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + // var found = false; + // foreach (var subscription in subscriptions) + // { + // if (subscription.TypeSubscription.TopicType == "TestEvent") + // { + // found = true; + // } + // } + // Assert.True(found); + // await agent.RemoveSubscriptionAsync("TestEvent").ConfigureAwait(true); + // await Task.Delay(1000); + // subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + // found = false; + // foreach (var subscription in subscriptions) + // { + // if (subscription.TypeSubscription.TopicType == "TestEvent") + // { + // found = true; + // } + // } + // Assert.False(found); + //} + + ///// + ///// Test StoreAsync and ReadAsync methods + ///// + ///// void + //[Fact] + //public async Task StoreAsync_and_ReadAsyncTest() + //{ + // using var runtime = new GrpcRuntime(); + // var (_, agent) = runtime.Start(); + // Dictionary state = new() + // { + // { "testdata", "Active" } + // }; + // await agent.StoreAsync(new AgentState + // { + // AgentId = agent.AgentId, + // TextData = JsonSerializer.Serialize(state) + // }).ConfigureAwait(true); + // var readState = await agent.ReadAsync(agent.AgentId).ConfigureAwait(true); + // var read = JsonSerializer.Deserialize>(readState.TextData) ?? new Dictionary { { "data", "No state data found" } }; + // read.TryGetValue("testdata", out var value); + // Assert.Equal("Active", value); + //} + + ///// + ///// Test PublishMessageAsync method and ReceiveMessage method + ///// + ///// void + //[Fact] + //public async Task PublishMessageAsync_and_ReceiveMessageTest() + //{ + // using var runtime = new GrpcRuntime(); + // var (_, agent) = runtime.Start(); + // var topicType = "TestTopic"; + // await agent.AddSubscriptionAsync(topicType).ConfigureAwait(true); + // var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + // var found = false; + // foreach (var subscription in subscriptions) + // { + // if (subscription.TypeSubscription.TopicType == topicType) + // { + // found = true; + // } + // } + // Assert.True(found); + // await agent.PublishMessageAsync(new TextMessage() + // { + // Source = topicType, + // TextMessage_ = "buffer" + // }, topicType).ConfigureAwait(true); + // await Task.Delay(100); + // Assert.True(TestAgent.ReceivedMessages.ContainsKey(topicType)); + // runtime.Stop(); + //} + + //[Fact] + //public async Task InvokeCorrectHandler() + //{ + // var agent = new TestAgent(new AgentsMetadata(TypeRegistry.Empty, new Dictionary(), new Dictionary>(), new Dictionary>()), new Logger(new LoggerFactory())); + + // await agent.HandleObjectAsync("hello world"); + // await agent.HandleObjectAsync(42); + + // agent.ReceivedItems.Should().HaveCount(2); + // agent.ReceivedItems[0].Should().Be("hello world"); + // agent.ReceivedItems[1].Should().Be(42); + //} +} + +/// +/// The test agent is a simple agent that is used for testing purposes. +/// +public class TestAgent(AgentId id, + IAgentRuntime runtime, + Logger? logger = null) : BaseAgent(id, runtime, "Test Agent", logger), + //IHandle, + //IHandle, + IHandle + +{ + //public ValueTask HandleAsync(TextMessage item, MessageContext messageContext) + //{ + // ReceivedMessages[item.Source] = item.Content; + // return ValueTask.CompletedTask; + //} + + public ValueTask HandleAsync(string item, MessageContext messageContext) { - using var runtime = new GrpcRuntime(); - var (_, agent) = runtime.Start(); - var topicType = "TestTopic"; - await agent.AddSubscriptionAsync(topicType).ConfigureAwait(true); - var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - var found = false; - foreach (var subscription in subscriptions) - { - if (subscription.TypeSubscription.TopicType == topicType) - { - found = true; - } - } - Assert.True(found); - await agent.PublishMessageAsync(new TextMessage() - { - Source = topicType, - TextMessage_ = "buffer" - }, topicType).ConfigureAwait(true); - await Task.Delay(100); - Assert.True(TestAgent.ReceivedMessages.ContainsKey(topicType)); - runtime.Stop(); + ReceivedItems.Add(item); + return ValueTask.CompletedTask; } - [Fact] - public async Task InvokeCorrectHandler() + public ValueTask HandleAsync(int item, MessageContext messageContext) { - var agent = new TestAgent(new AgentsMetadata(TypeRegistry.Empty, new Dictionary(), new Dictionary>(), new Dictionary>()), new Logger(new LoggerFactory())); + ReceivedItems.Add(item); + return ValueTask.CompletedTask; + } - await agent.HandleObjectAsync("hello world"); - await agent.HandleObjectAsync(42); + //public ValueTask HandleAsync(RpcTextMessage item, MessageContext messageContext) + //{ + // ReceivedMessages[item.Source] = item.Content; + // return ValueTask.FromResult(item.Content); + //} - agent.ReceivedItems.Should().HaveCount(2); - agent.ReceivedItems[0].Should().Be("hello world"); - agent.ReceivedItems[1].Should().Be(42); - } + public List ReceivedItems { get; private set; } = []; /// - /// The test agent is a simple agent that is used for testing purposes. + /// Key: source + /// Value: message /// - public class TestAgent( - [FromKeyedServices("AgentsMetadata")] AgentsMetadata eventTypes, - Logger? logger = null) : Agent(eventTypes, logger), IHandle - { - public Task Handle(TextMessage item, CancellationToken cancellationToken = default) - { - ReceivedMessages[item.Source] = item.TextMessage_; - return Task.CompletedTask; - } - public Task Handle(string item) - { - ReceivedItems.Add(item); - return Task.CompletedTask; - } - public Task Handle(int item) - { - ReceivedItems.Add(item); - return Task.CompletedTask; - } - public List ReceivedItems { get; private set; } = []; + public static Dictionary ReceivedMessages { get; private set; } = new(); +} - /// - /// Key: source - /// Value: message - /// - public static ConcurrentDictionary ReceivedMessages { get; private set; } = new(); +[TypeSubscription("TestTopic")] +public class SubscribedAgent : TestAgent +{ + public SubscribedAgent(AgentId id, + IAgentRuntime runtime, + Logger? logger = null) : base(id, runtime, logger) + { } } @@ -212,14 +242,18 @@ private static int GetAvailablePort() private static async Task StartClientAsync() { - return await AgentsApp.StartAsync().ConfigureAwait(false); - } - private static async Task StartAppHostAsync() - { - return await Microsoft.AutoGen.Runtime.Grpc.Host.StartAsync(local: false, useGrpc: true).ConfigureAwait(false); + AgentsApp agentsApp = await new AgentsAppBuilder().UseGrpcRuntime().AddAgent("TestAgent").BuildAsync(); + await agentsApp.StartAsync(); + + return agentsApp.Host; } + //private static async Task StartAppHostAsync() + //{ + // return await Microsoft.AutoGen.Runtime.Grpc.Host.StartAsync(local: false, useGrpc: true).ConfigureAwait(false); + //} + /// /// Start - gets a new port and starts fresh instances /// @@ -231,14 +265,14 @@ private static async Task StartAppHostAsync() Environment.SetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS", port.ToString()); Environment.SetEnvironmentVariable("AGENT_HOST", $"https://localhost:{port}"); - AppHost = StartAppHostAsync().GetAwaiter().GetResult(); + //AppHost = StartAppHostAsync().GetAwaiter().GetResult(); Client = StartClientAsync().GetAwaiter().GetResult(); var agent = ActivatorUtilities.CreateInstance(Client.Services); var worker = Client.Services.GetRequiredService(); if (initialize) { - Agent.Initialize(worker, agent); + //Agent.Initialize(worker, agent); } return (worker, agent); diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj index f14497e75fbc..a2dad2212a7f 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj @@ -10,7 +10,7 @@ - + From 414c4079e2cd76a5e4c6ccd3b8445d453479ac91 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 30 Jan 2025 08:40:28 -0500 Subject: [PATCH 07/25] fixup: Remove commented out Payload serialization code --- dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs index f0be2376a153..4302d81f6a06 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -107,7 +107,7 @@ public GrpcAgentRuntime(AgentRpc.AgentRpcClient client, private readonly CancellationTokenSource _shutdownCts; private readonly AgentsContainer _agentsContainer; - + public IServiceProvider ServiceProvider { get; } private string _clientId = Guid.NewGuid().ToString(); @@ -242,7 +242,7 @@ private async ValueTask HandlePublish(CloudEvent evt, CancellationToken cancella var agent = await this._agentsContainer.EnsureAgentAsync(sender); await agent.OnMessageAsync(message, messageContext); } - + public ValueTask StartAsync(CancellationToken cancellationToken) { return this._messageRouter.StartAsync(cancellationToken); From 7c3c9342e63c4322dab7261d3cff9ea30a463ae0 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 30 Jan 2025 08:46:28 -0500 Subject: [PATCH 08/25] fixup: Merge Builder extension method classes * Prefer AgentsAppBuilderExtensions as name, due to matching the incoming `this` type. --- .../Core.Grpc/AgentsAppBuilderExtensions.cs | 65 +++++++++++++++-- .../GrpcAgentWorkerHostBuilderExtension.cs | 70 ------------------- 2 files changed, 60 insertions(+), 75 deletions(-) delete mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs index e19cc2f343d2..8b11cc3da354 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs @@ -1,21 +1,76 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // AgentsAppBuilderExtensions.cs +using System.Diagnostics; +using Grpc.Core; +using Grpc.Net.Client.Configuration; using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Protobuf; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; - +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; namespace Microsoft.AutoGen.Core.Grpc; public static class AgentsAppBuilderExtensions { - public static AgentsAppBuilder UseGrpcRuntime(this AgentsAppBuilder this_, bool deliverToSelf = false) + private const string _defaultAgentServiceAddress = "https://localhost:53071"; + + // TODO: How do we ensure AddGrpcAgentWorker and UseInProcessRuntime are mutually exclusive? + public static AgentsAppBuilder AddGrpcAgentWorker(this AgentsAppBuilder builder, string? agentServiceAddress = null) { - this_.Services.AddSingleton(); - this_.Services.AddHostedService(services => + builder.Services.AddGrpcClient(options => + { + options.Address = new Uri(agentServiceAddress ?? builder.Configuration.GetValue("AGENT_HOST", _defaultAgentServiceAddress)); + options.ChannelOptionsActions.Add(channelOptions => + { + var loggerFactory = new LoggerFactory(); + if (Debugger.IsAttached) + { + channelOptions.HttpHandler = new SocketsHttpHandler + { + EnableMultipleHttp2Connections = false, + KeepAlivePingDelay = TimeSpan.FromSeconds(200), + KeepAlivePingTimeout = TimeSpan.FromSeconds(100), + KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always + }; + } + else + { + channelOptions.HttpHandler = new SocketsHttpHandler + { + EnableMultipleHttp2Connections = true, + KeepAlivePingDelay = TimeSpan.FromSeconds(20), + KeepAlivePingTimeout = TimeSpan.FromSeconds(10), + KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests + }; + } + + var methodConfig = new MethodConfig + { + Names = { MethodName.Default }, + RetryPolicy = new RetryPolicy + { + MaxAttempts = 5, + InitialBackoff = TimeSpan.FromSeconds(1), + MaxBackoff = TimeSpan.FromSeconds(5), + BackoffMultiplier = 1.5, + RetryableStatusCodes = { StatusCode.Unavailable } + } + }; + + channelOptions.ServiceConfig = new() { MethodConfigs = { methodConfig } }; + channelOptions.ThrowOperationCanceledOnCancellation = true; + }); + }); + + builder.Services.TryAddSingleton(DistributedContextPropagator.Current); + builder.Services.AddSingleton(); + builder.Services.AddHostedService(services => { return (services.GetRequiredService() as GrpcAgentRuntime)!; }); - return this_; + return builder; } } diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs deleted file mode 100644 index 7f43b9620f54..000000000000 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorkerHostBuilderExtension.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// GrpcAgentWorkerHostBuilderExtension.cs -using System.Diagnostics; -using Grpc.Core; -using Grpc.Net.Client.Configuration; -using Microsoft.AutoGen.Contracts; -using Microsoft.AutoGen.Protobuf; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -namespace Microsoft.AutoGen.Core.Grpc; - -public static class GrpcAgentWorkerHostBuilderExtensions -{ - private const string _defaultAgentServiceAddress = "https://localhost:53071"; - - // TODO: How do we ensure AddGrpcAgentWorker and UseInProcessRuntime are mutually exclusive? - public static AgentsAppBuilder AddGrpcAgentWorker(this AgentsAppBuilder builder, string? agentServiceAddress = null) - { - builder.Services.AddGrpcClient(options => - { - options.Address = new Uri(agentServiceAddress ?? builder.Configuration["AGENT_HOST"] ?? _defaultAgentServiceAddress); - options.ChannelOptionsActions.Add(channelOptions => - { - var loggerFactory = new LoggerFactory(); - if (Debugger.IsAttached) - { - channelOptions.HttpHandler = new SocketsHttpHandler - { - EnableMultipleHttp2Connections = false, - KeepAlivePingDelay = TimeSpan.FromSeconds(200), - KeepAlivePingTimeout = TimeSpan.FromSeconds(100), - KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always - }; - } - else - { - channelOptions.HttpHandler = new SocketsHttpHandler - { - EnableMultipleHttp2Connections = true, - KeepAlivePingDelay = TimeSpan.FromSeconds(20), - KeepAlivePingTimeout = TimeSpan.FromSeconds(10), - KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests - }; - } - - var methodConfig = new MethodConfig - { - Names = { MethodName.Default }, - RetryPolicy = new RetryPolicy - { - MaxAttempts = 5, - InitialBackoff = TimeSpan.FromSeconds(1), - MaxBackoff = TimeSpan.FromSeconds(5), - BackoffMultiplier = 1.5, - RetryableStatusCodes = { StatusCode.Unavailable } - } - }; - - channelOptions.ServiceConfig = new() { MethodConfigs = { methodConfig } }; - channelOptions.ThrowOperationCanceledOnCancellation = true; - }); - }); - builder.Services.TryAddSingleton(DistributedContextPropagator.Current); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => (IHostedService)sp.GetRequiredService()); - return builder; - } -} From 72168b57a7f4695ed800341914a41d6d2bfa9d54 Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Mon, 3 Feb 2025 14:57:34 -0500 Subject: [PATCH 09/25] Cleanup constants, put back rpc based impl --- .../Core.Grpc/CloudEventExtensions.cs | 39 ++++++ .../Microsoft.AutoGen/Core.Grpc/Constants.cs | 21 ++++ .../Core.Grpc/GrpcAgentRuntime.cs | 118 ++++-------------- .../Core.Grpc/GrpcMessageRouter.cs | 2 +- .../Core.Grpc/IAgentMessageSerializer.cs | 2 +- ...lizer.cs => IProtobufMessageSerializer.cs} | 6 +- .../Core.Grpc/ISerializationRegistry.cs | 6 +- .../Core.Grpc/ITypeNameResolver.cs | 2 +- .../Core.Grpc/ProtobufConversionExtensions.cs | 3 +- .../Core.Grpc/ProtobufMessageSerializer.cs | 4 +- ...ry.cs => ProtobufSerializationRegistry.cs} | 16 +-- ...esolver.cs => ProtobufTypeNameResolver.cs} | 4 +- .../Core.Grpc/RpcExtensions.cs | 43 +++++++ 13 files changed, 147 insertions(+), 119 deletions(-) create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/CloudEventExtensions.cs create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/Constants.cs rename dotnet/src/Microsoft.AutoGen/Core.Grpc/{IProtoMessageSerializer.cs => IProtobufMessageSerializer.cs} (74%) rename dotnet/src/Microsoft.AutoGen/Core.Grpc/{ProtoSerializationRegistry.cs => ProtobufSerializationRegistry.cs} (56%) rename dotnet/src/Microsoft.AutoGen/Core.Grpc/{ProtoTypeNameResolver.cs => ProtobufTypeNameResolver.cs} (82%) create mode 100644 dotnet/src/Microsoft.AutoGen/Core.Grpc/RpcExtensions.cs diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/CloudEventExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/CloudEventExtensions.cs new file mode 100644 index 000000000000..cab48d971316 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/CloudEventExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// CloudEventExtensions.cs + +using Microsoft.AutoGen.Contracts; + +namespace Microsoft.AutoGen.Core.Grpc; + +internal static class CloudEventExtensions +{ + // Convert an ISubscrptionDefinition to a Protobuf Subscription + internal static CloudEvent CreateCloudEvent(Google.Protobuf.WellKnownTypes.Any payload, TopicId topic, string dataType, AgentId sender, string messageId) + { + return new CloudEvent + { + ProtoData = payload, + Type = topic.Type, + Source = topic.Source, + Id = messageId, + Attributes = { + { + Constants.DATA_CONTENT_TYPE_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = Constants.DATA_CONTENT_TYPE_PROTOBUF_VALUE } + }, + { + Constants.DATA_SCHEMA_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = dataType } + }, + { + Constants.AGENT_SENDER_TYPE_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = sender.Type } + }, + { + Constants.AGENT_SENDER_KEY_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = sender.Key } + }, + { + Constants.MESSAGE_KIND_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = Constants.MESSAGE_KIND_VALUE_PUBLISH } + } + } + }; + + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/Constants.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/Constants.cs new file mode 100644 index 000000000000..c3e9592c1dc2 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/Constants.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Constants.cs + +namespace Microsoft.AutoGen.Core.Grpc; + +public static class Constants +{ + public const string DATA_CONTENT_TYPE_PROTOBUF_VALUE = "application/x-protobuf"; + public const string DATA_CONTENT_TYPE_JSON_VALUE = "application/json"; + public const string DATA_CONTENT_TYPE_TEXT_VALUE = "text/plain"; + + public const string DATA_CONTENT_TYPE_ATTR = "datacontenttype"; + public const string DATA_SCHEMA_ATTR = "dataschema"; + public const string AGENT_SENDER_TYPE_ATTR = "agagentsendertype"; + public const string AGENT_SENDER_KEY_ATTR = "agagentsenderkey"; + + public const string MESSAGE_KIND_ATTR = "agmsgkind"; + public const string MESSAGE_KIND_VALUE_PUBLISH = "publish"; + public const string MESSAGE_KIND_VALUE_RPC_REQUEST = "rpc_request"; + public const string MESSAGE_KIND_VALUE_RPC_RESPONSE = "rpc_response"; +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs index 4302d81f6a06..9126a4f895cd 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -2,12 +2,11 @@ // GrpcAgentRuntime.cs using System.Collections.Concurrent; -using Google.Protobuf; using Grpc.Core; using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Protobuf; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.AutoGen.Protobuf; namespace Microsoft.AutoGen.Core.Grpc; @@ -80,7 +79,7 @@ public bool RemoveSubscriptionAsync(string subscriptionId) public IEnumerable LiveAgents => this.agentInstances.Values; } -public sealed class GrpcAgentRuntime: IHostedService, IAgentRuntime, IMessageSink, IDisposable +public sealed class GrpcAgentRuntime : IHostedService, IAgentRuntime, IMessageSink, IDisposable { public GrpcAgentRuntime(AgentRpc.AgentRpcClient client, IHostApplicationLifetime hostApplicationLifetime, @@ -123,7 +122,7 @@ private CallOptions CallOptions } } - public IProtoSerializationRegistry SerializationRegistry { get; } = new ProtoSerializationRegistry(); + public IProtoSerializationRegistry SerializationRegistry { get; } = new ProtobufSerializationRegistry(); public void Dispose() { @@ -155,7 +154,7 @@ private async ValueTask HandleRequest(RpcRequest request, CancellationToken canc // Convert payload back to object var payload = request.Payload; - var message = PayloadToObject(payload); + var message = payload.ToObject(SerializationRegistry); var messageContext = new MessageContext(request.RequestId, cancellationToken) { @@ -171,7 +170,7 @@ private async ValueTask HandleRequest(RpcRequest request, CancellationToken canc var response = new RpcResponse { RequestId = request.RequestId, - Payload = ObjectToPayload(result) + Payload = result.ToPayload(SerializationRegistry) }; var responseMessage = new Message @@ -201,7 +200,7 @@ private async ValueTask HandleResponse(RpcResponse request, CancellationToken _ if (_pendingRequests.TryRemove(request.RequestId, out var resultSink)) { var payload = request.Payload; - var message = PayloadToObject(payload); + var message = payload.ToObject(SerializationRegistry); resultSink.SetResult(message); } } @@ -224,12 +223,12 @@ private async ValueTask HandlePublish(CloudEvent evt, CancellationToken cancella var topic = new TopicId(evt.Type, evt.Source); var sender = new Contracts.AgentId { - Type = evt.Attributes["agagentsendertype"].CeString, - Key = evt.Attributes["agagentsenderkey"].CeString + Type = evt.Attributes[Constants.AGENT_SENDER_TYPE_ATTR].CeString, + Key = evt.Attributes[Constants.AGENT_SENDER_KEY_ATTR].CeString }; var messageId = evt.Id; - var typeName = evt.Attributes["dataschema"].CeString; + var typeName = evt.Attributes[Constants.DATA_SCHEMA_ATTR].CeString; var serializer = SerializationRegistry.GetSerializer(typeName) ?? throw new Exception(); var message = serializer.Deserialize(evt.ProtoData); @@ -255,36 +254,6 @@ public Task StopAsync(CancellationToken cancellationToken) return this._messageRouter.StopAsync(); } - private Payload ObjectToPayload(object message) { - if (!SerializationRegistry.Exists(message.GetType())) - { - SerializationRegistry.RegisterSerializer(message.GetType()); - } - var rpcMessage = (SerializationRegistry.GetSerializer(message.GetType()) ?? throw new Exception()).Serialize(message); - - var typeName = SerializationRegistry.TypeNameResolver.ResolveTypeName(message); - const string PAYLOAD_DATA_CONTENT_TYPE = "application/x-protobuf"; - - // Protobuf any to byte array - Payload payload = new() - { - DataType = typeName, - DataContentType = PAYLOAD_DATA_CONTENT_TYPE, - Data = rpcMessage.ToByteString() - }; - - return payload; - } - - private object PayloadToObject(Payload payload) { - var typeName = payload.DataType; - var data = payload.Data; - var type = SerializationRegistry.TypeNameResolver.ResolveTypeName(typeName); - var serializer = SerializationRegistry.GetSerializer(type) ?? throw new Exception(); - var any = Google.Protobuf.WellKnownTypes.Any.Parser.ParseFrom(data); - return serializer.Deserialize(any); - } - public async ValueTask SendMessageAsync(object message, Contracts.AgentId recepient, Contracts.AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) { if (!SerializationRegistry.Exists(message.GetType())) @@ -292,11 +261,11 @@ private object PayloadToObject(Payload payload) { SerializationRegistry.RegisterSerializer(message.GetType()); } - var payload = ObjectToPayload(message); + var payload = message.ToPayload(SerializationRegistry); var request = new RpcRequest { RequestId = Guid.NewGuid().ToString(), - Source = (sender ?? new Contracts.AgentId() ).ToProtobuf(), + Source = (sender ?? new Contracts.AgentId()).ToProtobuf(), Target = recepient.ToProtobuf(), Payload = payload, }; @@ -314,35 +283,6 @@ private object PayloadToObject(Payload payload) { return await resultSink.Future; } - private CloudEvent CreateCloudEvent(Google.Protobuf.WellKnownTypes.Any payload, TopicId topic, string dataType, Contracts.AgentId sender, string messageId) - { - const string PAYLOAD_DATA_CONTENT_TYPE = "application/x-protobuf"; - return new CloudEvent - { - ProtoData = payload, - Type = topic.Type, - Source = topic.Source, - Id = messageId, - Attributes = { - { - "datacontenttype", new CloudEvent.Types.CloudEventAttributeValue { CeString = PAYLOAD_DATA_CONTENT_TYPE } - }, - { - "dataschema", new CloudEvent.Types.CloudEventAttributeValue { CeString = dataType } - }, - { - "agagentsendertype", new CloudEvent.Types.CloudEventAttributeValue { CeString = sender.Type } - }, - { - "agagentsenderkey", new CloudEvent.Types.CloudEventAttributeValue { CeString = sender.Key } - }, - { - "agmsgkind", new CloudEvent.Types.CloudEventAttributeValue { CeString = "publish" } - } - } - }; - } - public async ValueTask PublishMessageAsync(object message, TopicId topic, Contracts.AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) { if (!SerializationRegistry.Exists(message.GetType())) @@ -352,7 +292,7 @@ public async ValueTask PublishMessageAsync(object message, TopicId topic, Contra var protoAny = (SerializationRegistry.GetSerializer(message.GetType()) ?? throw new Exception()).Serialize(message); var typeName = SerializationRegistry.TypeNameResolver.ResolveTypeName(message); - var cloudEvent = CreateCloudEvent(protoAny, topic, typeName, sender ?? new Contracts.AgentId(), messageId ?? Guid.NewGuid().ToString()); + var cloudEvent = CloudEventExtensions.CreateCloudEvent(protoAny, topic, typeName, sender ?? new Contracts.AgentId(), messageId ?? Guid.NewGuid().ToString()); Message msg = new() { @@ -382,38 +322,24 @@ public async ValueTask GetAgentMetadataAsync(Contracts.AgentId ag return agent.Metadata; } - public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription) + public async ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription) { this._agentsContainer.AddSubscription(subscription); - // Because we have an extensible definition of ISubscriptionDefinition, we cannot project it to the Gateway. - // What this means is that we will have a much chattier interface between the Gateway and the Runtime. - - //await this._client.AddSubscriptionAsync(new AddSubscriptionRequest - //{ - // Subscription = new Subscription - // { - // Id = subscription.Id, - // TopicType = subscription.TopicType, - // AgentType = subscription.AgentType.Name - // } - //}, this.CallOptions); - - return ValueTask.CompletedTask; + var _ = await this._client.AddSubscriptionAsync(new AddSubscriptionRequest + { + Subscription = subscription.ToProtobuf() + }, this.CallOptions); } - public ValueTask RemoveSubscriptionAsync(string subscriptionId) + public async ValueTask RemoveSubscriptionAsync(string subscriptionId) { this._agentsContainer.RemoveSubscriptionAsync(subscriptionId); - // See above (AddSubscriptionAsync) for why this is commented out. - - //await this._client.RemoveSubscriptionAsync(new RemoveSubscriptionRequest - //{ - // Id = subscriptionId - //}, this.CallOptions); - - return ValueTask.CompletedTask; + await this._client.RemoveSubscriptionAsync(new RemoveSubscriptionRequest + { + Id = subscriptionId + }, this.CallOptions); } public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs index 0ecf8ee1413e..9fdf0b608785 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs @@ -3,8 +3,8 @@ using System.Threading.Channels; using Grpc.Core; -using Microsoft.Extensions.Logging; using Microsoft.AutoGen.Protobuf; +using Microsoft.Extensions.Logging; namespace Microsoft.AutoGen.Core.Grpc; diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentMessageSerializer.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentMessageSerializer.cs index 0cc422d54d85..c2ca53e33710 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentMessageSerializer.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IAgentMessageSerializer.cs @@ -20,4 +20,4 @@ public interface IAgentMessageSerializer /// The message to deserialize. /// The deserialized message. object Deserialize(Google.Protobuf.WellKnownTypes.Any message); -} \ No newline at end of file +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtoMessageSerializer.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtobufMessageSerializer.cs similarity index 74% rename from dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtoMessageSerializer.cs rename to dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtobufMessageSerializer.cs index ca690e508d2b..7d92614b7c3f 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtoMessageSerializer.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/IProtobufMessageSerializer.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// IProtoMessageSerializer.cs +// IProtobufMessageSerializer.cs namespace Microsoft.AutoGen.Core.Grpc; -public interface IProtoMessageSerializer +public interface IProtobufMessageSerializer { Google.Protobuf.WellKnownTypes.Any Serialize(object input); object Deserialize(Google.Protobuf.WellKnownTypes.Any input); -} \ No newline at end of file +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ISerializationRegistry.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ISerializationRegistry.cs index 190ed3ec239d..c736a1c38cde 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ISerializationRegistry.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ISerializationRegistry.cs @@ -11,15 +11,15 @@ public interface IProtoSerializationRegistry /// The type to register. void RegisterSerializer(System.Type type) => RegisterSerializer(type, new ProtobufMessageSerializer(type)); - void RegisterSerializer(System.Type type, IProtoMessageSerializer serializer); + void RegisterSerializer(System.Type type, IProtobufMessageSerializer serializer); /// /// Gets the serializer for the specified type. /// /// The type to get the serializer for. /// The serializer for the specified type. - IProtoMessageSerializer? GetSerializer(System.Type type) => GetSerializer(TypeNameResolver.ResolveTypeName(type)); - IProtoMessageSerializer? GetSerializer(string typeName); + IProtobufMessageSerializer? GetSerializer(System.Type type) => GetSerializer(TypeNameResolver.ResolveTypeName(type)); + IProtobufMessageSerializer? GetSerializer(string typeName); ITypeNameResolver TypeNameResolver { get; } diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs index 24de4cb8b449..3b40633c4f0f 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs @@ -6,4 +6,4 @@ namespace Microsoft.AutoGen.Core.Grpc; public interface ITypeNameResolver { string ResolveTypeName(object input); -} \ No newline at end of file +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufConversionExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufConversionExtensions.cs index 4850b7825afe..3175817c0eee 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufConversionExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufConversionExtensions.cs @@ -57,5 +57,4 @@ public static Protobuf.AgentId ToProtobuf(this Contracts.AgentId agentId) Key = agentId.Key }; } - -} \ No newline at end of file +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufMessageSerializer.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufMessageSerializer.cs index 55c1aebfa47d..09da49640ad0 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufMessageSerializer.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufMessageSerializer.cs @@ -9,7 +9,7 @@ namespace Microsoft.AutoGen.Core.Grpc; /// /// Interface for serializing and deserializing agent messages. /// -public class ProtobufMessageSerializer : IProtoMessageSerializer +public class ProtobufMessageSerializer : IProtobufMessageSerializer { private System.Type _concreteType; @@ -43,4 +43,4 @@ public Any Serialize(object message) // Raise an exception if the message is not a proto IMessage throw new ArgumentException("Message must be a proto IMessage", nameof(message)); } -} \ No newline at end of file +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoSerializationRegistry.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufSerializationRegistry.cs similarity index 56% rename from dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoSerializationRegistry.cs rename to dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufSerializationRegistry.cs index e744bcb0eee9..1bc0449d5688 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoSerializationRegistry.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufSerializationRegistry.cs @@ -1,32 +1,32 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// ProtoSerializationRegistry.cs +// ProtobufSerializationRegistry.cs namespace Microsoft.AutoGen.Core.Grpc; -public class ProtoSerializationRegistry : IProtoSerializationRegistry +public class ProtobufSerializationRegistry : IProtoSerializationRegistry { - private readonly Dictionary _serializers - = new Dictionary(); + private readonly Dictionary _serializers + = new Dictionary(); - public ITypeNameResolver TypeNameResolver => new ProtoTypeNameResolver(); + public ITypeNameResolver TypeNameResolver => new ProtobufTypeNameResolver(); public bool Exists(Type type) { return _serializers.ContainsKey(TypeNameResolver.ResolveTypeName(type)); } - public IProtoMessageSerializer? GetSerializer(Type type) + public IProtobufMessageSerializer? GetSerializer(Type type) { return GetSerializer(TypeNameResolver.ResolveTypeName(type)); } - public IProtoMessageSerializer? GetSerializer(string typeName) + public IProtobufMessageSerializer? GetSerializer(string typeName) { _serializers.TryGetValue(typeName, out var serializer); return serializer; } - public void RegisterSerializer(Type type, IProtoMessageSerializer serializer) + public void RegisterSerializer(Type type, IProtobufMessageSerializer serializer) { if (_serializers.ContainsKey(TypeNameResolver.ResolveTypeName(type))) { diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoTypeNameResolver.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufTypeNameResolver.cs similarity index 82% rename from dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoTypeNameResolver.cs rename to dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufTypeNameResolver.cs index a769b0f31c81..5d75bb879f56 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtoTypeNameResolver.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufTypeNameResolver.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// ProtoTypeNameResolver.cs +// ProtobufTypeNameResolver.cs using Google.Protobuf; namespace Microsoft.AutoGen.Core.Grpc; -public class ProtoTypeNameResolver : ITypeNameResolver +public class ProtobufTypeNameResolver : ITypeNameResolver { public string ResolveTypeName(object input) { diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/RpcExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/RpcExtensions.cs new file mode 100644 index 000000000000..a1794f9cdfef --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/RpcExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RpcExtensions.cs + +using Google.Protobuf; +using Microsoft.AutoGen.Protobuf; + +namespace Microsoft.AutoGen.Core.Grpc; + +internal static class RpcExtensions +{ + + public static Payload ToPayload(this object message, IProtoSerializationRegistry serializationRegistry) + { + if (!serializationRegistry.Exists(message.GetType())) + { + serializationRegistry.RegisterSerializer(message.GetType()); + } + var rpcMessage = (serializationRegistry.GetSerializer(message.GetType()) ?? throw new Exception()).Serialize(message); + + var typeName = serializationRegistry.TypeNameResolver.ResolveTypeName(message); + const string PAYLOAD_DATA_CONTENT_TYPE = "application/x-protobuf"; + + // Protobuf any to byte array + Payload payload = new() + { + DataType = typeName, + DataContentType = PAYLOAD_DATA_CONTENT_TYPE, + Data = rpcMessage.ToByteString() + }; + + return payload; + } + + public static object ToObject(this Payload payload, IProtoSerializationRegistry serializationRegistry) + { + var typeName = payload.DataType; + var data = payload.Data; + var type = serializationRegistry.TypeNameResolver.ResolveTypeName(typeName); + var serializer = serializationRegistry.GetSerializer(type) ?? throw new Exception(); + var any = Google.Protobuf.WellKnownTypes.Any.Parser.ParseFrom(data); + return serializer.Deserialize(any); + } +} From f87497b6121c45391502baa6b504bca590b20e59 Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Mon, 3 Feb 2025 16:37:56 -0500 Subject: [PATCH 10/25] Finish implementation including initial manual testing --- dotnet/AutoGen.sln | 7 +++ dotnet/samples/GettingStartedGrpc/Checker.cs | 34 +++++++++++ .../GettingStartedGrpc.csproj | 26 +++++++++ dotnet/samples/GettingStartedGrpc/Modifier.cs | 29 ++++++++++ dotnet/samples/GettingStartedGrpc/Program.cs | 36 ++++++++++++ .../samples/GettingStartedGrpc/message.proto | 11 ++++ .../Core.Grpc/AgentsAppBuilderExtensions.cs | 2 +- .../Core.Grpc/CloudEventExtensions.cs | 40 +++++++------ .../Core.Grpc/GrpcAgentRuntime.cs | 58 +++++++++++++------ .../Core.Grpc/GrpcMessageRouter.cs | 12 +++- .../Core.Grpc/ITypeNameResolver.cs | 2 +- .../Core.Grpc/ProtobufTypeNameResolver.cs | 6 +- .../Core.Grpc/RpcExtensions.cs | 5 +- 13 files changed, 223 insertions(+), 45 deletions(-) create mode 100644 dotnet/samples/GettingStartedGrpc/Checker.cs create mode 100644 dotnet/samples/GettingStartedGrpc/GettingStartedGrpc.csproj create mode 100644 dotnet/samples/GettingStartedGrpc/Modifier.cs create mode 100644 dotnet/samples/GettingStartedGrpc/Program.cs create mode 100644 dotnet/samples/GettingStartedGrpc/message.proto diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln index ab7a07464c52..21344f506e9c 100644 --- a/dotnet/AutoGen.sln +++ b/dotnet/AutoGen.sln @@ -118,6 +118,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hello", "Hello", "{F42F9C8E EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Core.Grpc", "src\Microsoft.AutoGen\Core.Grpc\Microsoft.AutoGen.Core.Grpc.csproj", "{3D83C6DB-ACEA-48F3-959F-145CCD2EE135}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStartedGrpc", "samples\GettingStartedGrpc\GettingStartedGrpc.csproj", "{C3740DF1-18B1-4607-81E4-302F0308C848}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -306,6 +308,10 @@ Global {AAD593FE-A49B-425E-A9FE-A0022CD25E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {AAD593FE-A49B-425E-A9FE-A0022CD25E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {AAD593FE-A49B-425E-A9FE-A0022CD25E3D}.Release|Any CPU.Build.0 = Release|Any CPU + {C3740DF1-18B1-4607-81E4-302F0308C848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3740DF1-18B1-4607-81E4-302F0308C848}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3740DF1-18B1-4607-81E4-302F0308C848}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3740DF1-18B1-4607-81E4-302F0308C848}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -359,6 +365,7 @@ Global {3D83C6DB-ACEA-48F3-959F-145CCD2EE135} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {AAD593FE-A49B-425E-A9FE-A0022CD25E3D} = {F42F9C8E-7BD9-4687-9B63-AFFA461AF5C1} {F42F9C8E-7BD9-4687-9B63-AFFA461AF5C1} = {CE0AA8D5-12B8-4628-9589-DAD8CB0DDCF6} + {C3740DF1-18B1-4607-81E4-302F0308C848} = {CE0AA8D5-12B8-4628-9589-DAD8CB0DDCF6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B} diff --git a/dotnet/samples/GettingStartedGrpc/Checker.cs b/dotnet/samples/GettingStartedGrpc/Checker.cs new file mode 100644 index 000000000000..7f75acbfafd6 --- /dev/null +++ b/dotnet/samples/GettingStartedGrpc/Checker.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Checker.cs + +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Core; +using Microsoft.Extensions.Hosting; +using TerminationF = System.Func; + +namespace GettingStartedGrpcSample; + +[TypeSubscription("default")] +public class Checker( + AgentId id, + IAgentRuntime runtime, + IHostApplicationLifetime hostApplicationLifetime, + TerminationF runUntilFunc + ) : + BaseAgent(id, runtime, "Modifier", null), + IHandle +{ + public async ValueTask HandleAsync(Events.CountUpdate item, MessageContext messageContext) + { + if (!runUntilFunc(item.NewCount)) + { + Console.WriteLine($"\nChecker:\n{item.NewCount} passed the check, continue."); + await this.PublishMessageAsync(new Events.CountMessage { Content = item.NewCount }, new TopicId("default")); + } + else + { + Console.WriteLine($"\nChecker:\n{item.NewCount} failed the check, stopping."); + hostApplicationLifetime.StopApplication(); + } + } +} diff --git a/dotnet/samples/GettingStartedGrpc/GettingStartedGrpc.csproj b/dotnet/samples/GettingStartedGrpc/GettingStartedGrpc.csproj new file mode 100644 index 000000000000..a419cd2fe906 --- /dev/null +++ b/dotnet/samples/GettingStartedGrpc/GettingStartedGrpc.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + getting_started + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStartedGrpc/Modifier.cs b/dotnet/samples/GettingStartedGrpc/Modifier.cs new file mode 100644 index 000000000000..ad3a9d8d97a6 --- /dev/null +++ b/dotnet/samples/GettingStartedGrpc/Modifier.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Modifier.cs + +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Core; + +using ModifyF = System.Func; + +namespace GettingStartedGrpcSample; + +[TypeSubscription("default")] +public class Modifier( + AgentId id, + IAgentRuntime runtime, + ModifyF modifyFunc + ) : + BaseAgent(id, runtime, "Modifier", null), + IHandle +{ + + public async ValueTask HandleAsync(Events.CountMessage item, MessageContext messageContext) + { + int newValue = modifyFunc(item.Content); + Console.WriteLine($"\nModifier:\nModified {item.Content} to {newValue}"); + + var updateMessage = new Events.CountUpdate { NewCount = newValue }; + await this.PublishMessageAsync(updateMessage, topic: new TopicId("default")); + } +} diff --git a/dotnet/samples/GettingStartedGrpc/Program.cs b/dotnet/samples/GettingStartedGrpc/Program.cs new file mode 100644 index 000000000000..aa9cc5417082 --- /dev/null +++ b/dotnet/samples/GettingStartedGrpc/Program.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Program.cs +using GettingStartedGrpcSample; +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Core; +using Microsoft.AutoGen.Core.Grpc; +using Microsoft.Extensions.DependencyInjection.Extensions; +using ModifyF = System.Func; +using TerminationF = System.Func; + +ModifyF modifyFunc = (int x) => x - 1; +TerminationF runUntilFunc = (int x) => +{ + return x <= 1; +}; + +AgentsAppBuilder appBuilder = new AgentsAppBuilder(); +appBuilder.AddGrpcAgentWorker("http://localhost:50051"); + +appBuilder.Services.TryAddSingleton(modifyFunc); +appBuilder.Services.TryAddSingleton(runUntilFunc); + +appBuilder.AddAgent("Checker"); +appBuilder.AddAgent("Modifier"); + +var app = await appBuilder.BuildAsync(); +await app.StartAsync(); + +// Send the initial count to the agents app, running on the `local` runtime, and pass through the registered services via the application `builder` +await app.PublishMessageAsync(new GettingStartedGrpcSample.Events.CountMessage +{ + Content = 10 +}, new TopicId("default")); + +// Run until application shutdown +await app.WaitForShutdownAsync(); diff --git a/dotnet/samples/GettingStartedGrpc/message.proto b/dotnet/samples/GettingStartedGrpc/message.proto new file mode 100644 index 000000000000..d4acac2e5711 --- /dev/null +++ b/dotnet/samples/GettingStartedGrpc/message.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +option csharp_namespace = "GettingStartedGrpcSample.Events"; + +message CountMessage { + int32 content = 1; +} + +message CountUpdate { + int32 new_count = 1; +} diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs index 8b11cc3da354..7cf66d4cee9d 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/AgentsAppBuilderExtensions.cs @@ -14,7 +14,7 @@ namespace Microsoft.AutoGen.Core.Grpc; public static class AgentsAppBuilderExtensions { - private const string _defaultAgentServiceAddress = "https://localhost:53071"; + private const string _defaultAgentServiceAddress = "http://localhost:53071"; // TODO: How do we ensure AddGrpcAgentWorker and UseInProcessRuntime are mutually exclusive? public static AgentsAppBuilder AddGrpcAgentWorker(this AgentsAppBuilder builder, string? agentServiceAddress = null) diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/CloudEventExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/CloudEventExtensions.cs index cab48d971316..1ee46660ce8e 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/CloudEventExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/CloudEventExtensions.cs @@ -8,31 +8,35 @@ namespace Microsoft.AutoGen.Core.Grpc; internal static class CloudEventExtensions { // Convert an ISubscrptionDefinition to a Protobuf Subscription - internal static CloudEvent CreateCloudEvent(Google.Protobuf.WellKnownTypes.Any payload, TopicId topic, string dataType, AgentId sender, string messageId) + internal static CloudEvent CreateCloudEvent(Google.Protobuf.WellKnownTypes.Any payload, TopicId topic, string dataType, AgentId? sender, string messageId) { + var attributes = new Dictionary + { + { + Constants.DATA_CONTENT_TYPE_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = Constants.DATA_CONTENT_TYPE_PROTOBUF_VALUE } + }, + { + Constants.DATA_SCHEMA_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = dataType } + }, + { + Constants.MESSAGE_KIND_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = Constants.MESSAGE_KIND_VALUE_PUBLISH } + } + }; + + if (sender != null) + { + var senderNonNull = (AgentId)sender; + attributes.Add(Constants.AGENT_SENDER_TYPE_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = senderNonNull.Type }); + attributes.Add(Constants.AGENT_SENDER_KEY_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = senderNonNull.Key }); + } + return new CloudEvent { ProtoData = payload, Type = topic.Type, Source = topic.Source, Id = messageId, - Attributes = { - { - Constants.DATA_CONTENT_TYPE_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = Constants.DATA_CONTENT_TYPE_PROTOBUF_VALUE } - }, - { - Constants.DATA_SCHEMA_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = dataType } - }, - { - Constants.AGENT_SENDER_TYPE_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = sender.Type } - }, - { - Constants.AGENT_SENDER_KEY_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = sender.Key } - }, - { - Constants.MESSAGE_KIND_ATTR, new CloudEvent.Types.CloudEventAttributeValue { CeString = Constants.MESSAGE_KIND_VALUE_PUBLISH } - } - } + Attributes = { attributes } }; } diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs index 9126a4f895cd..5f1fc7785345 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -15,7 +15,7 @@ internal sealed class AgentsContainer(IAgentRuntime hostingRuntime) private readonly IAgentRuntime hostingRuntime = hostingRuntime; private Dictionary agentInstances = new(); - private Dictionary subscriptions = new(); + public Dictionary Subscriptions = new(); private Dictionary>> agentFactories = new(); public async ValueTask EnsureAgentAsync(Contracts.AgentId agentId) @@ -57,22 +57,22 @@ public AgentType RegisterAgentFactory(AgentType type, Func RegisteredAgentTypes => this.agentFactories.Keys.ToHashSet(); @@ -90,7 +90,7 @@ public GrpcAgentRuntime(AgentRpc.AgentRpcClient client, this._logger = logger; this._shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); - this._messageRouter = new GrpcMessageRouter(client, this, logger, this._shutdownCts.Token); + this._messageRouter = new GrpcMessageRouter(client, this, _clientId, logger, this._shutdownCts.Token); this._agentsContainer = new AgentsContainer(this); this.ServiceProvider = serviceProvider; @@ -109,14 +109,14 @@ public GrpcAgentRuntime(AgentRpc.AgentRpcClient client, public IServiceProvider ServiceProvider { get; } - private string _clientId = Guid.NewGuid().ToString(); + private Guid _clientId = Guid.NewGuid(); private CallOptions CallOptions { get { var metadata = new Metadata { - { "client-id", this._clientId } + { "client-id", this._clientId.ToString() } }; return new CallOptions(headers: metadata); } @@ -221,11 +221,15 @@ private async ValueTask HandlePublish(CloudEvent evt, CancellationToken cancella } var topic = new TopicId(evt.Type, evt.Source); - var sender = new Contracts.AgentId + Contracts.AgentId? sender = null; + if (evt.Attributes.TryGetValue(Constants.AGENT_SENDER_TYPE_ATTR, out var typeValue) && evt.Attributes.TryGetValue(Constants.AGENT_SENDER_KEY_ATTR, out var keyValue)) { - Type = evt.Attributes[Constants.AGENT_SENDER_TYPE_ATTR].CeString, - Key = evt.Attributes[Constants.AGENT_SENDER_KEY_ATTR].CeString - }; + sender = new Contracts.AgentId + { + Type = typeValue.CeString, + Key = keyValue.CeString + }; + } var messageId = evt.Id; var typeName = evt.Attributes[Constants.DATA_SCHEMA_ATTR].CeString; @@ -238,8 +242,17 @@ private async ValueTask HandlePublish(CloudEvent evt, CancellationToken cancella Topic = topic, IsRpc = false }; - var agent = await this._agentsContainer.EnsureAgentAsync(sender); - await agent.OnMessageAsync(message, messageContext); + + // Iterate over subscriptions values to find receiving agents + foreach (var subscription in this._agentsContainer.Subscriptions.Values) + { + if (subscription.Matches(topic)) + { + var recipient = subscription.MapToAgent(topic); + var agent = await this._agentsContainer.EnsureAgentAsync(recipient); + await agent.OnMessageAsync(message, messageContext); + } + } } public ValueTask StartAsync(CancellationToken cancellationToken) @@ -290,9 +303,9 @@ public async ValueTask PublishMessageAsync(object message, TopicId topic, Contra SerializationRegistry.RegisterSerializer(message.GetType()); } var protoAny = (SerializationRegistry.GetSerializer(message.GetType()) ?? throw new Exception()).Serialize(message); - var typeName = SerializationRegistry.TypeNameResolver.ResolveTypeName(message); + var typeName = SerializationRegistry.TypeNameResolver.ResolveTypeName(message.GetType()); - var cloudEvent = CloudEventExtensions.CreateCloudEvent(protoAny, topic, typeName, sender ?? new Contracts.AgentId(), messageId ?? Guid.NewGuid().ToString()); + var cloudEvent = CloudEventExtensions.CreateCloudEvent(protoAny, topic, typeName, sender, messageId ?? Guid.NewGuid().ToString()); Message msg = new() { @@ -342,8 +355,17 @@ await this._client.RemoveSubscriptionAsync(new RemoveSubscriptionRequest }, this.CallOptions); } - public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) - => ValueTask.FromResult(this._agentsContainer.RegisterAgentFactory(type, factoryFunc)); + public async ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) + { + this._agentsContainer.RegisterAgentFactory(type, factoryFunc); + + await this._client.RegisterAgentAsync(new RegisterAgentTypeRequest + { + Type = type, + }, this.CallOptions); + + return type; + } public ValueTask TryGetAgentProxyAsync(Contracts.AgentId agentId) { diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs index 9fdf0b608785..e46b392c708f 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcMessageRouter.cs @@ -18,15 +18,18 @@ internal sealed class AutoRestartChannel : IDisposable { private readonly object _channelLock = new(); private readonly AgentRpc.AgentRpcClient _client; + private readonly Guid _clientId; private readonly ILogger _logger; private readonly CancellationTokenSource _shutdownCts; private AsyncDuplexStreamingCall? _channel; public AutoRestartChannel(AgentRpc.AgentRpcClient client, + Guid clientId, ILogger logger, CancellationToken shutdownCancellation = default) { _client = client; + _clientId = clientId; _logger = logger; _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellation); } @@ -73,8 +76,12 @@ private AsyncDuplexStreamingCall RecreateChannel(AsyncDuplexSt { if (_channel is null || _channel == ownedChannel) { + var metadata = new Metadata + { + { "client-id", _clientId.ToString() } + }; _channel?.Dispose(); - _channel = _client.OpenChannel(cancellationToken: _shutdownCts.Token); + _channel = _client.OpenChannel(cancellationToken: _shutdownCts.Token, headers: metadata); } } } @@ -91,6 +98,7 @@ public void Dispose() internal sealed class GrpcMessageRouter(AgentRpc.AgentRpcClient client, IMessageSink incomingMessageSink, + Guid clientId, ILogger logger, CancellationToken shutdownCancellation = default) : IDisposable { @@ -111,7 +119,7 @@ internal sealed class GrpcMessageRouter(AgentRpc.AgentRpcClient client, // TODO: Enable a way to configure the channel options = Channel.CreateBounded<(Message, TaskCompletionSource)>(DefaultChannelOptions); - private readonly AutoRestartChannel _incomingMessageChannel = new AutoRestartChannel(client, logger, shutdownCancellation); + private readonly AutoRestartChannel _incomingMessageChannel = new AutoRestartChannel(client, clientId, logger, shutdownCancellation); private Task? _readTask; private Task? _writeTask; diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs index 3b40633c4f0f..67ba1c577f4a 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ITypeNameResolver.cs @@ -5,5 +5,5 @@ namespace Microsoft.AutoGen.Core.Grpc; public interface ITypeNameResolver { - string ResolveTypeName(object input); + string ResolveTypeName(Type input); } diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufTypeNameResolver.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufTypeNameResolver.cs index 5d75bb879f56..e376f9a13daa 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufTypeNameResolver.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/ProtobufTypeNameResolver.cs @@ -7,10 +7,12 @@ namespace Microsoft.AutoGen.Core.Grpc; public class ProtobufTypeNameResolver : ITypeNameResolver { - public string ResolveTypeName(object input) + public string ResolveTypeName(Type input) { - if (input is IMessage protoMessage) + if (typeof(IMessage).IsAssignableFrom(input)) { + // TODO: Consider changing this to avoid instantiation... + var protoMessage = (IMessage?)Activator.CreateInstance(input) ?? throw new InvalidOperationException($"Failed to create instance of {input.FullName}"); return protoMessage.Descriptor.FullName; } else diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/RpcExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/RpcExtensions.cs index a1794f9cdfef..5c264887856c 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/RpcExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/RpcExtensions.cs @@ -17,7 +17,7 @@ public static Payload ToPayload(this object message, IProtoSerializationRegistry } var rpcMessage = (serializationRegistry.GetSerializer(message.GetType()) ?? throw new Exception()).Serialize(message); - var typeName = serializationRegistry.TypeNameResolver.ResolveTypeName(message); + var typeName = serializationRegistry.TypeNameResolver.ResolveTypeName(message.GetType()); const string PAYLOAD_DATA_CONTENT_TYPE = "application/x-protobuf"; // Protobuf any to byte array @@ -35,8 +35,7 @@ public static object ToObject(this Payload payload, IProtoSerializationRegistry { var typeName = payload.DataType; var data = payload.Data; - var type = serializationRegistry.TypeNameResolver.ResolveTypeName(typeName); - var serializer = serializationRegistry.GetSerializer(type) ?? throw new Exception(); + var serializer = serializationRegistry.GetSerializer(typeName) ?? throw new Exception(); var any = Google.Protobuf.WellKnownTypes.Any.Parser.ParseFrom(data); return serializer.Deserialize(any); } From f8f7093cb9dbc36425adf5b09c8c62c00daf382b Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Mon, 3 Feb 2025 16:41:19 -0500 Subject: [PATCH 11/25] remove unrelated files --- .../AgentGrpcTests.cs | 346 ++++++++---------- .../Microsoft.AutoGen.Core.Grpc.Tests.csproj | 2 +- .../AgentTests.cs | 2 +- 3 files changed, 158 insertions(+), 192 deletions(-) diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs index bb054ee738a4..7514609e145b 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -1,214 +1,184 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // AgentGrpcTests.cs -//using System.Collections.Concurrent; -//using System.Text.Json; -//using FluentAssertions; -//using Google.Protobuf.Reflection; +using System.Collections.Concurrent; +using System.Text.Json; +using FluentAssertions; +using Google.Protobuf.Reflection; using Microsoft.AutoGen.Contracts; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit; +using static Microsoft.AutoGen.Core.Grpc.Tests.AgentGrpcTests; namespace Microsoft.AutoGen.Core.Grpc.Tests; [Trait("Category", "UnitV2")] public class AgentGrpcTests { + /// + /// Verify that if the agent is not initialized via AgentWorker, it should throw the correct exception. + /// + /// void [Fact] - public void Agent_ShouldInitializeCorrectly() + public async Task Agent_ShouldThrowException_WhenNotInitialized() + { + using var runtime = new GrpcRuntime(); + var (_, agent) = runtime.Start(false); // Do not initialize + + // Expect an exception when calling AddSubscriptionAsync because the agent is uninitialized + await Assert.ThrowsAsync( + async () => await agent.AddSubscriptionAsync("TestEvent") + ); + } + + /// + /// validate that the agent is initialized correctly with implicit subs + /// + /// void + [Fact] + public async Task Agent_ShouldInitializeCorrectly() { using var runtime = new GrpcRuntime(); var (worker, agent) = runtime.Start(); Assert.Equal(nameof(GrpcAgentRuntime), worker.GetType().Name); + await Task.Delay(5000); + var subscriptions = await agent.GetSubscriptionsAsync(); + Assert.Equal(2, subscriptions.Count); + } + /// + /// Test AddSubscriptionAsync method + /// + /// void + [Fact] + public async Task SubscribeAsync_UnsubscribeAsync_and_GetSubscriptionsTest() + { + using var runtime = new GrpcRuntime(); + var (_, agent) = runtime.Start(); + await agent.AddSubscriptionAsync("TestEvent"); + await Task.Delay(100); + var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + var found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == "TestEvent") + { + found = true; + } + } + Assert.True(found); + await agent.RemoveSubscriptionAsync("TestEvent").ConfigureAwait(true); + await Task.Delay(1000); + subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == "TestEvent") + { + found = true; + } + } + Assert.False(found); } - ///// - ///// Verify that if the agent is not initialized via AgentWorker, it should throw the correct exception. - ///// - ///// void - //[Fact] - //public async Task Agent_ShouldThrowException_WhenNotInitialized() - //{ - // using var runtime = new GrpcRuntime(); - // var (_, agent) = runtime.Start(false); // Do not initialize - - // // Expect an exception when calling AddSubscriptionAsync because the agent is uninitialized - // await Assert.ThrowsAsync( - // async () => await agent.AddSubscriptionAsync("TestEvent") - // ); - //} - - ///// - ///// validate that the agent is initialized correctly with implicit subs - ///// - ///// void - //[Fact] - //public async Task Agent_ShouldInitializeCorrectly() - //{ - // using var runtime = new GrpcRuntime(); - // var (worker, agent) = runtime.Start(); - // Assert.Equal(nameof(GrpcAgentRuntime), worker.GetType().Name); - // await Task.Delay(5000); - // var subscriptions = await agent.GetSubscriptionsAsync(); - // Assert.Equal(2, subscriptions.Count); - //} - ///// - ///// Test AddSubscriptionAsync method - ///// - ///// void - //[Fact] - //public async Task SubscribeAsync_UnsubscribeAsync_and_GetSubscriptionsTest() - //{ - // using var runtime = new GrpcRuntime(); - // var (_, agent) = runtime.Start(); - // await agent.AddSubscriptionAsync("TestEvent"); - // await Task.Delay(100); - // var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - // var found = false; - // foreach (var subscription in subscriptions) - // { - // if (subscription.TypeSubscription.TopicType == "TestEvent") - // { - // found = true; - // } - // } - // Assert.True(found); - // await agent.RemoveSubscriptionAsync("TestEvent").ConfigureAwait(true); - // await Task.Delay(1000); - // subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - // found = false; - // foreach (var subscription in subscriptions) - // { - // if (subscription.TypeSubscription.TopicType == "TestEvent") - // { - // found = true; - // } - // } - // Assert.False(found); - //} - - ///// - ///// Test StoreAsync and ReadAsync methods - ///// - ///// void - //[Fact] - //public async Task StoreAsync_and_ReadAsyncTest() - //{ - // using var runtime = new GrpcRuntime(); - // var (_, agent) = runtime.Start(); - // Dictionary state = new() - // { - // { "testdata", "Active" } - // }; - // await agent.StoreAsync(new AgentState - // { - // AgentId = agent.AgentId, - // TextData = JsonSerializer.Serialize(state) - // }).ConfigureAwait(true); - // var readState = await agent.ReadAsync(agent.AgentId).ConfigureAwait(true); - // var read = JsonSerializer.Deserialize>(readState.TextData) ?? new Dictionary { { "data", "No state data found" } }; - // read.TryGetValue("testdata", out var value); - // Assert.Equal("Active", value); - //} - - ///// - ///// Test PublishMessageAsync method and ReceiveMessage method - ///// - ///// void - //[Fact] - //public async Task PublishMessageAsync_and_ReceiveMessageTest() - //{ - // using var runtime = new GrpcRuntime(); - // var (_, agent) = runtime.Start(); - // var topicType = "TestTopic"; - // await agent.AddSubscriptionAsync(topicType).ConfigureAwait(true); - // var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - // var found = false; - // foreach (var subscription in subscriptions) - // { - // if (subscription.TypeSubscription.TopicType == topicType) - // { - // found = true; - // } - // } - // Assert.True(found); - // await agent.PublishMessageAsync(new TextMessage() - // { - // Source = topicType, - // TextMessage_ = "buffer" - // }, topicType).ConfigureAwait(true); - // await Task.Delay(100); - // Assert.True(TestAgent.ReceivedMessages.ContainsKey(topicType)); - // runtime.Stop(); - //} - - //[Fact] - //public async Task InvokeCorrectHandler() - //{ - // var agent = new TestAgent(new AgentsMetadata(TypeRegistry.Empty, new Dictionary(), new Dictionary>(), new Dictionary>()), new Logger(new LoggerFactory())); - - // await agent.HandleObjectAsync("hello world"); - // await agent.HandleObjectAsync(42); - - // agent.ReceivedItems.Should().HaveCount(2); - // agent.ReceivedItems[0].Should().Be("hello world"); - // agent.ReceivedItems[1].Should().Be(42); - //} -} - -/// -/// The test agent is a simple agent that is used for testing purposes. -/// -public class TestAgent(AgentId id, - IAgentRuntime runtime, - Logger? logger = null) : BaseAgent(id, runtime, "Test Agent", logger), - //IHandle, - //IHandle, - IHandle - -{ - //public ValueTask HandleAsync(TextMessage item, MessageContext messageContext) - //{ - // ReceivedMessages[item.Source] = item.Content; - // return ValueTask.CompletedTask; - //} - - public ValueTask HandleAsync(string item, MessageContext messageContext) + /// + /// Test StoreAsync and ReadAsync methods + /// + /// void + [Fact] + public async Task StoreAsync_and_ReadAsyncTest() { - ReceivedItems.Add(item); - return ValueTask.CompletedTask; + using var runtime = new GrpcRuntime(); + var (_, agent) = runtime.Start(); + Dictionary state = new() + { + { "testdata", "Active" } + }; + await agent.StoreAsync(new AgentState + { + AgentId = agent.AgentId, + TextData = JsonSerializer.Serialize(state) + }).ConfigureAwait(true); + var readState = await agent.ReadAsync(agent.AgentId).ConfigureAwait(true); + var read = JsonSerializer.Deserialize>(readState.TextData) ?? new Dictionary { { "data", "No state data found" } }; + read.TryGetValue("testdata", out var value); + Assert.Equal("Active", value); } - public ValueTask HandleAsync(int item, MessageContext messageContext) + /// + /// Test PublishMessageAsync method and ReceiveMessage method + /// + /// void + [Fact] + public async Task PublishMessageAsync_and_ReceiveMessageTest() { - ReceivedItems.Add(item); - return ValueTask.CompletedTask; + using var runtime = new GrpcRuntime(); + var (_, agent) = runtime.Start(); + var topicType = "TestTopic"; + await agent.AddSubscriptionAsync(topicType).ConfigureAwait(true); + var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); + var found = false; + foreach (var subscription in subscriptions) + { + if (subscription.TypeSubscription.TopicType == topicType) + { + found = true; + } + } + Assert.True(found); + await agent.PublishMessageAsync(new TextMessage() + { + Source = topicType, + TextMessage_ = "buffer" + }, topicType).ConfigureAwait(true); + await Task.Delay(100); + Assert.True(TestAgent.ReceivedMessages.ContainsKey(topicType)); + runtime.Stop(); } - //public ValueTask HandleAsync(RpcTextMessage item, MessageContext messageContext) - //{ - // ReceivedMessages[item.Source] = item.Content; - // return ValueTask.FromResult(item.Content); - //} + [Fact] + public async Task InvokeCorrectHandler() + { + var agent = new TestAgent(new AgentsMetadata(TypeRegistry.Empty, new Dictionary(), new Dictionary>(), new Dictionary>()), new Logger(new LoggerFactory())); + + await agent.HandleObjectAsync("hello world"); + await agent.HandleObjectAsync(42); - public List ReceivedItems { get; private set; } = []; + agent.ReceivedItems.Should().HaveCount(2); + agent.ReceivedItems[0].Should().Be("hello world"); + agent.ReceivedItems[1].Should().Be(42); + } /// - /// Key: source - /// Value: message + /// The test agent is a simple agent that is used for testing purposes. /// - public static Dictionary ReceivedMessages { get; private set; } = new(); -} - -[TypeSubscription("TestTopic")] -public class SubscribedAgent : TestAgent -{ - public SubscribedAgent(AgentId id, - IAgentRuntime runtime, - Logger? logger = null) : base(id, runtime, logger) + public class TestAgent( + [FromKeyedServices("AgentsMetadata")] AgentsMetadata eventTypes, + Logger? logger = null) : Agent(eventTypes, logger), IHandle { + public Task Handle(TextMessage item, CancellationToken cancellationToken = default) + { + ReceivedMessages[item.Source] = item.TextMessage_; + return Task.CompletedTask; + } + public Task Handle(string item) + { + ReceivedItems.Add(item); + return Task.CompletedTask; + } + public Task Handle(int item) + { + ReceivedItems.Add(item); + return Task.CompletedTask; + } + public List ReceivedItems { get; private set; } = []; + + /// + /// Key: source + /// Value: message + /// + public static ConcurrentDictionary ReceivedMessages { get; private set; } = new(); } } @@ -242,17 +212,13 @@ private static int GetAvailablePort() private static async Task StartClientAsync() { - AgentsApp agentsApp = await new AgentsAppBuilder().UseGrpcRuntime().AddAgent("TestAgent").BuildAsync(); - - await agentsApp.StartAsync(); - - return agentsApp.Host; + return await AgentsApp.StartAsync().ConfigureAwait(false); } + private static async Task StartAppHostAsync() + { + return await Microsoft.AutoGen.Runtime.Grpc.Host.StartAsync(local: false, useGrpc: true).ConfigureAwait(false); - //private static async Task StartAppHostAsync() - //{ - // return await Microsoft.AutoGen.Runtime.Grpc.Host.StartAsync(local: false, useGrpc: true).ConfigureAwait(false); - //} + } /// /// Start - gets a new port and starts fresh instances @@ -265,14 +231,14 @@ private static async Task StartClientAsync() Environment.SetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS", port.ToString()); Environment.SetEnvironmentVariable("AGENT_HOST", $"https://localhost:{port}"); - //AppHost = StartAppHostAsync().GetAwaiter().GetResult(); + AppHost = StartAppHostAsync().GetAwaiter().GetResult(); Client = StartClientAsync().GetAwaiter().GetResult(); var agent = ActivatorUtilities.CreateInstance(Client.Services); var worker = Client.Services.GetRequiredService(); if (initialize) { - //Agent.Initialize(worker, agent); + Agent.Initialize(worker, agent); } return (worker, agent); diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj index a2dad2212a7f..f14497e75fbc 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs index da81f40a6f3c..cc39b3564c66 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs @@ -121,7 +121,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => }); Assert.Null(agent); - await runtime.GetAgentAsync(AgentId.FromStr("MyAgent"), lazy: false); + await runtime.GetAgentAsync("MyAgent", lazy: false); Assert.NotNull(agent); Assert.True(agent.ReceivedItems.Count == 0); From 7fb61ca434e1a5025de48849851a0ee15606424d Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Mon, 3 Feb 2025 16:57:17 -0500 Subject: [PATCH 12/25] fix build --- dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs | 6 ++---- dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs | 6 ++++++ dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs | 6 ++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs index bb360617dc09..0d84fbe72d37 100644 --- a/dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Contracts/IAgentRuntime.cs @@ -53,8 +53,7 @@ public interface IAgentRuntime : ISaveState /// An optional key to specify variations of the agent. Defaults to "default". /// If true, the agent is fetched lazily. /// A task representing the asynchronous operation, returning the agent's ID. - public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/) - => this.GetAgentAsync(new AgentId(agentType, key), lazy); + public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/); /// /// Retrieves an agent by its string representation. @@ -63,8 +62,7 @@ public ValueTask GetAgentAsync(AgentType agentType, string key = "defau /// An optional key to specify variations of the agent. Defaults to "default". /// If true, the agent is fetched lazily. /// A task representing the asynchronous operation, returning the agent's ID. - public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/) - => this.GetAgentAsync(new AgentId(agent, key), lazy); + public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/); /// /// Saves the state of an agent. diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs index 5f1fc7785345..2f2057f6aa62 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -317,6 +317,12 @@ public async ValueTask PublishMessageAsync(object message, TopicId topic, Contra public ValueTask GetAgentAsync(Contracts.AgentId agentId, bool lazy = true) => this._agentsContainer.GetAgentAsync(agentId, lazy); + public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true) + => this.GetAgentAsync(new Contracts.AgentId(agentType, key), lazy); + + public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true) + => this.GetAgentAsync(new Contracts.AgentId(agent, key), lazy); + public async ValueTask> SaveAgentStateAsync(Contracts.AgentId agentId) { IHostableAgent agent = await this._agentsContainer.EnsureAgentAsync(agentId); diff --git a/dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs index 791376ae56e8..69b2d314e550 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/InProcessRuntime.cs @@ -140,6 +140,12 @@ public async ValueTask GetAgentAsync(AgentId agentId, bool lazy = true) return agentId; } + public ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true) + => this.GetAgentAsync(new AgentId(agentType, key), lazy); + + public ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true) + => this.GetAgentAsync(new AgentId(agent, key), lazy); + public async ValueTask GetAgentMetadataAsync(AgentId agentId) { IHostableAgent agent = await this.EnsureAgentAsync(agentId); From 734230bc45ff1f8389b2c4b918ebc2ec701cf0de Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Mon, 3 Feb 2025 22:24:26 -0800 Subject: [PATCH 13/25] interim - adding tests for GrpcAgentRuntime --- dotnet/AutoGen.sln | 7 + .../AgentGrpcTests.cs | 327 +++++++----------- .../GrpcAgentRuntimeFixture.cs | 83 +++++ .../GrpcAgentServiceFixture.cs | 60 ++++ .../GrpcWorkerConnection.cs | 120 +++++++ .../Microsoft.AutoGen.Core.Grpc.Tests.csproj | 2 +- 6 files changed, 389 insertions(+), 210 deletions(-) create mode 100644 dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs create mode 100644 dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs create mode 100644 dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcWorkerConnection.cs diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln index 21344f506e9c..74b7ac965592 100644 --- a/dotnet/AutoGen.sln +++ b/dotnet/AutoGen.sln @@ -120,6 +120,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Core.Grpc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStartedGrpc", "samples\GettingStartedGrpc\GettingStartedGrpc.csproj", "{C3740DF1-18B1-4607-81E4-302F0308C848}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Core.Grpc.Tests", "test\Microsoft.AutoGen.Core.Grpc.Tests\Microsoft.AutoGen.Core.Grpc.Tests.csproj", "{23A028D3-5EB1-4FA0-9CD1-A1340B830579}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -312,6 +314,10 @@ Global {C3740DF1-18B1-4607-81E4-302F0308C848}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3740DF1-18B1-4607-81E4-302F0308C848}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3740DF1-18B1-4607-81E4-302F0308C848}.Release|Any CPU.Build.0 = Release|Any CPU + {23A028D3-5EB1-4FA0-9CD1-A1340B830579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23A028D3-5EB1-4FA0-9CD1-A1340B830579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23A028D3-5EB1-4FA0-9CD1-A1340B830579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23A028D3-5EB1-4FA0-9CD1-A1340B830579}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -366,6 +372,7 @@ Global {AAD593FE-A49B-425E-A9FE-A0022CD25E3D} = {F42F9C8E-7BD9-4687-9B63-AFFA461AF5C1} {F42F9C8E-7BD9-4687-9B63-AFFA461AF5C1} = {CE0AA8D5-12B8-4628-9589-DAD8CB0DDCF6} {C3740DF1-18B1-4607-81E4-302F0308C848} = {CE0AA8D5-12B8-4628-9589-DAD8CB0DDCF6} + {23A028D3-5EB1-4FA0-9CD1-A1340B830579} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs index 7514609e145b..4a8507822d7d 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -1,263 +1,172 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // AgentGrpcTests.cs - -using System.Collections.Concurrent; -using System.Text.Json; using FluentAssertions; -using Google.Protobuf.Reflection; using Microsoft.AutoGen.Contracts; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.AutoGen.Core.Tests; + using Xunit; -using static Microsoft.AutoGen.Core.Grpc.Tests.AgentGrpcTests; namespace Microsoft.AutoGen.Core.Grpc.Tests; [Trait("Category", "UnitV2")] public class AgentGrpcTests { - /// - /// Verify that if the agent is not initialized via AgentWorker, it should throw the correct exception. - /// - /// void [Fact] - public async Task Agent_ShouldThrowException_WhenNotInitialized() + public async Task AgentShouldNotReceiveMessagesWhenNotSubscribedTest() { - using var runtime = new GrpcRuntime(); - var (_, agent) = runtime.Start(false); // Do not initialize + var fixture = new GrpcAgentRuntimeFixture(); + var runtime = (GrpcAgentRuntime)await fixture.Start(); - // Expect an exception when calling AddSubscriptionAsync because the agent is uninitialized - await Assert.ThrowsAsync( - async () => await agent.AddSubscriptionAsync("TestEvent") - ); - } + Logger logger = new(new LoggerFactory()); + TestAgent agent = null!; - /// - /// validate that the agent is initialized correctly with implicit subs - /// - /// void - [Fact] - public async Task Agent_ShouldInitializeCorrectly() - { - using var runtime = new GrpcRuntime(); - var (worker, agent) = runtime.Start(); - Assert.Equal(nameof(GrpcAgentRuntime), worker.GetType().Name); - await Task.Delay(5000); - var subscriptions = await agent.GetSubscriptionsAsync(); - Assert.Equal(2, subscriptions.Count); - } - /// - /// Test AddSubscriptionAsync method - /// - /// void - [Fact] - public async Task SubscribeAsync_UnsubscribeAsync_and_GetSubscriptionsTest() - { - using var runtime = new GrpcRuntime(); - var (_, agent) = runtime.Start(); - await agent.AddSubscriptionAsync("TestEvent"); - await Task.Delay(100); - var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - var found = false; - foreach (var subscription in subscriptions) + await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => { - if (subscription.TypeSubscription.TopicType == "TestEvent") - { - found = true; - } - } - Assert.True(found); - await agent.RemoveSubscriptionAsync("TestEvent").ConfigureAwait(true); - await Task.Delay(1000); - subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - found = false; - foreach (var subscription in subscriptions) - { - if (subscription.TypeSubscription.TopicType == "TestEvent") - { - found = true; - } - } - Assert.False(found); + agent = new TestAgent(id, runtime, logger); + return await ValueTask.FromResult(agent); + }); + + // Ensure the agent is actually created + AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + + // Validate agent ID + agentId.Should().Be(agent.Id, "Agent ID should match the registered agent"); + + var topicType = "TestTopic"; + + await runtime.PublishMessageAsync(new Core.Tests.TextMessage { Source = topicType, Content = "test" }, new TopicId(topicType)).ConfigureAwait(true); + + agent.ReceivedMessages.Any().Should().BeFalse("Agent should not receive messages when not subscribed."); + fixture.Dispose(); } - /// - /// Test StoreAsync and ReadAsync methods - /// - /// void [Fact] - public async Task StoreAsync_and_ReadAsyncTest() + public async Task AgentShouldReceiveMessagesWhenSubscribedTest() { - using var runtime = new GrpcRuntime(); - var (_, agent) = runtime.Start(); - Dictionary state = new() - { - { "testdata", "Active" } - }; - await agent.StoreAsync(new AgentState + var runtime = new InProcessRuntime(); + await runtime.StartAsync(); + + Logger logger = new(new LoggerFactory()); + SubscribedAgent agent = null!; + + await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => { - AgentId = agent.AgentId, - TextData = JsonSerializer.Serialize(state) - }).ConfigureAwait(true); - var readState = await agent.ReadAsync(agent.AgentId).ConfigureAwait(true); - var read = JsonSerializer.Deserialize>(readState.TextData) ?? new Dictionary { { "data", "No state data found" } }; - read.TryGetValue("testdata", out var value); - Assert.Equal("Active", value); - } + agent = new SubscribedAgent(id, runtime, logger); + return ValueTask.FromResult(agent); + }); + + // Ensure the agent is actually created + AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + + // Validate agent ID + agentId.Should().Be(agent.Id, "Agent ID should match the registered agent"); + + await runtime.RegisterImplicitAgentSubscriptionsAsync("MyAgent"); - /// - /// Test PublishMessageAsync method and ReceiveMessage method - /// - /// void - [Fact] - public async Task PublishMessageAsync_and_ReceiveMessageTest() - { - using var runtime = new GrpcRuntime(); - var (_, agent) = runtime.Start(); var topicType = "TestTopic"; - await agent.AddSubscriptionAsync(topicType).ConfigureAwait(true); - var subscriptions = await agent.GetSubscriptionsAsync().ConfigureAwait(true); - var found = false; - foreach (var subscription in subscriptions) - { - if (subscription.TypeSubscription.TopicType == topicType) - { - found = true; - } - } - Assert.True(found); - await agent.PublishMessageAsync(new TextMessage() - { - Source = topicType, - TextMessage_ = "buffer" - }, topicType).ConfigureAwait(true); - await Task.Delay(100); - Assert.True(TestAgent.ReceivedMessages.ContainsKey(topicType)); - runtime.Stop(); + + await runtime.PublishMessageAsync(new Core.Tests.TextMessage { Source = topicType, Content = "test" }, new TopicId(topicType)).ConfigureAwait(true); + + await runtime.RunUntilIdleAsync(); + + agent.ReceivedMessages.Any().Should().BeTrue("Agent should receive messages when subscribed."); } [Fact] - public async Task InvokeCorrectHandler() + public async Task SendMessageAsyncShouldReturnResponseTest() { - var agent = new TestAgent(new AgentsMetadata(TypeRegistry.Empty, new Dictionary(), new Dictionary>(), new Dictionary>()), new Logger(new LoggerFactory())); + // Arrange + var runtime = new InProcessRuntime(); + await runtime.StartAsync(); - await agent.HandleObjectAsync("hello world"); - await agent.HandleObjectAsync(42); + Logger logger = new(new LoggerFactory()); + await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => ValueTask.FromResult(new TestAgent(id, runtime, logger))); + await runtime.RegisterImplicitAgentSubscriptionsAsync("MyAgent"); - agent.ReceivedItems.Should().HaveCount(2); - agent.ReceivedItems[0].Should().Be("hello world"); - agent.ReceivedItems[1].Should().Be(42); - } + var agentId = new AgentId("MyAgent", "TestAgent"); - /// - /// The test agent is a simple agent that is used for testing purposes. - /// - public class TestAgent( - [FromKeyedServices("AgentsMetadata")] AgentsMetadata eventTypes, - Logger? logger = null) : Agent(eventTypes, logger), IHandle - { - public Task Handle(TextMessage item, CancellationToken cancellationToken = default) + var response = await runtime.SendMessageAsync(new RpcTextMessage { Source = "TestTopic", Content = "Request" }, agentId); + + // Assert + Assert.NotNull(response); + Assert.IsType(response); + if (response is string responseString) { - ReceivedMessages[item.Source] = item.TextMessage_; - return Task.CompletedTask; + Assert.Equal("Request", responseString); } - public Task Handle(string item) - { - ReceivedItems.Add(item); - return Task.CompletedTask; - } - public Task Handle(int item) + } + + public class ReceiverAgent(AgentId id, + IAgentRuntime runtime) : BaseAgent(id, runtime, "Receiver Agent", null), + IHandle + { + public ValueTask HandleAsync(string item, MessageContext messageContext) { ReceivedItems.Add(item); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - public List ReceivedItems { get; private set; } = []; - /// - /// Key: source - /// Value: message - /// - public static ConcurrentDictionary ReceivedMessages { get; private set; } = new(); + public List ReceivedItems { get; private set; } = []; } -} - -/// -/// GrpcRuntimeFixture - provides a fixture for the agent runtime. -/// -/// -/// This fixture is used to provide a runtime for the agent tests. -/// However, it is shared between tests. So operations from one test can affect another. -/// -public sealed class GrpcRuntime : IDisposable -{ - public IHost Client { get; private set; } - public IHost? AppHost { get; private set; } - public GrpcRuntime() + [Fact] + public async Task SubscribeAsyncRemoveSubscriptionAsyncAndGetSubscriptionsTest() { - Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); - AppHost = Host.CreateDefaultBuilder().Build(); - Client = Host.CreateDefaultBuilder().Build(); - } + var runtime = new InProcessRuntime(); + await runtime.StartAsync(); + ReceiverAgent? agent = null; + await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => + { + agent = new ReceiverAgent(id, runtime); + return ValueTask.FromResult(agent); + }); - private static int GetAvailablePort() - { - using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0); - listener.Start(); - int port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } + Assert.Null(agent); + await runtime.GetAgentAsync("MyAgent", lazy: false); + Assert.NotNull(agent); + Assert.True(agent.ReceivedItems.Count == 0); - private static async Task StartClientAsync() - { - return await AgentsApp.StartAsync().ConfigureAwait(false); - } - private static async Task StartAppHostAsync() - { - return await Microsoft.AutoGen.Runtime.Grpc.Host.StartAsync(local: false, useGrpc: true).ConfigureAwait(false); + var topicTypeName = "TestTopic"; + await runtime.PublishMessageAsync("info", new TopicId(topicTypeName)); + await Task.Delay(100); - } + Assert.True(agent.ReceivedItems.Count == 0); - /// - /// Start - gets a new port and starts fresh instances - /// - public (IAgentRuntime, TestAgent) Start(bool initialize = true) - { - int port = GetAvailablePort(); // Get a new port per test run + var subscription = new TypeSubscription(topicTypeName, "MyAgent"); + await runtime.AddSubscriptionAsync(subscription); - // Update environment variables so each test runs independently - Environment.SetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS", port.ToString()); - Environment.SetEnvironmentVariable("AGENT_HOST", $"https://localhost:{port}"); + await runtime.PublishMessageAsync("info", new TopicId(topicTypeName)); + await Task.Delay(100); - AppHost = StartAppHostAsync().GetAwaiter().GetResult(); - Client = StartClientAsync().GetAwaiter().GetResult(); + Assert.True(agent.ReceivedItems.Count == 1); + Assert.Equal("info", agent.ReceivedItems[0]); - var agent = ActivatorUtilities.CreateInstance(Client.Services); - var worker = Client.Services.GetRequiredService(); - if (initialize) - { - Agent.Initialize(worker, agent); - } + await runtime.RemoveSubscriptionAsync(subscription.Id); + await runtime.PublishMessageAsync("info", new TopicId(topicTypeName)); + await Task.Delay(100); - return (worker, agent); + Assert.True(agent.ReceivedItems.Count == 1); } - /// - /// Stop - stops the agent and ensures cleanup - /// - public void Stop() + [Fact] + public async Task AgentShouldSaveStateCorrectlyTest() { - Client?.StopAsync().GetAwaiter().GetResult(); - AppHost?.StopAsync().GetAwaiter().GetResult(); - } + var runtime = new InProcessRuntime(); + await runtime.StartAsync(); - /// - /// Dispose - Ensures cleanup after each test - /// - public void Dispose() - { - Stop(); + Logger logger = new(new LoggerFactory()); + TestAgent agent = new TestAgent(new AgentId("TestType", "TestKey"), runtime, logger); + + var state = await agent.SaveStateAsync(); + + // Ensure state is a dictionary + state.Should().NotBeNull(); + state.Should().BeOfType>(); + state.Should().BeEmpty("Default SaveStateAsync should return an empty dictionary."); + + // Add a sample value and verify it updates correctly + state["testKey"] = "testValue"; + state.Should().ContainKey("testKey").WhoseValue.Should().Be("testValue"); } } diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs new file mode 100644 index 000000000000..f9adaf0659b7 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GrpcAgentRuntimeFixture.cs +using Microsoft.AspNetCore.Builder; +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Core.Tests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AutoGen.Core.Grpc.Tests; +/// +/// Fixture for setting up the gRPC agent runtime for testing. +/// +public sealed class GrpcAgentRuntimeFixture : IDisposable +{ + /// the gRPC agent runtime. + public AgentsApp? Client { get; private set; } + /// mock server for testing. + public WebApplication? Server { get; private set; } + + public GrpcAgentRuntimeFixture() + { + } + /// + /// Start - gets a new port and starts fresh instances + /// + public async Task Start(bool initialize = true) + { + int port = GetAvailablePort(); // Get a new port per test run + + // Update environment variables so each test runs independently + Environment.SetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS", port.ToString()); + Environment.SetEnvironmentVariable("AGENT_HOST", $"https://localhost:{port}"); + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + Server = ServerBuilder().Result; + await Server.StartAsync().ConfigureAwait(true); + Client = ClientBuilder().Result; + await Client.StartAsync().ConfigureAwait(true); + + var worker = Client.Services.GetRequiredService(); + + return (worker); + } + private static async Task ClientBuilder() + { + var appBuilder = new AgentsAppBuilder(); + appBuilder.AddGrpcAgentWorker(); + appBuilder.AddAgent("TestAgent"); + return await appBuilder.BuildAsync(); + } + private static async Task ServerBuilder() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddGrpc(); + var app = builder.Build(); + app.MapGrpcService(); + return app; + } + private static int GetAvailablePort() + { + using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0); + listener.Start(); + int port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + /// + /// Stop - stops the agent and ensures cleanup + /// + public void Stop() + { + (Client as IHost)?.StopAsync(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult(); + Server?.StopAsync().GetAwaiter().GetResult(); + } + + /// + /// Dispose - Ensures cleanup after each test + /// + public void Dispose() + { + Stop(); + } + +} \ No newline at end of file diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs new file mode 100644 index 000000000000..83032d5026a8 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GrpcAgentServiceFixture.cs +using Grpc.Core; +using Microsoft.AutoGen.Protobuf; +namespace Microsoft.AutoGen.Core.Grpc.Tests; + +public sealed class GrpcAgentServiceFixture() : AgentRpc.AgentRpcBase +{ + + public override async Task OpenChannel(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + try + { + var workerProcess = new GrpcWorkerConnection(requestStream, responseStream, context); + await workerProcess.Connect().ConfigureAwait(true); + } + catch + { + if (context.CancellationToken.IsCancellationRequested) + { + return; + } + throw; + } + } + public override async Task GetState(AgentId request, ServerCallContext context) + { + return new GetStateResponse { AgentState = new AgentState { AgentId = request } }; + } + public override async Task SaveState(AgentState request, ServerCallContext context) + { + return new SaveStateResponse + { + }; + } + public override async Task AddSubscription(AddSubscriptionRequest request, ServerCallContext context) + { + return new AddSubscriptionResponse + { + }; + } + public override async Task RemoveSubscription(RemoveSubscriptionRequest request, ServerCallContext context) + { + return new RemoveSubscriptionResponse + { + }; + } + public override async Task GetSubscriptions(GetSubscriptionsRequest request, ServerCallContext context) + { + return new GetSubscriptionsResponse + { + }; + } + public override async Task RegisterAgent(RegisterAgentTypeRequest request, ServerCallContext context) + { + return new RegisterAgentTypeResponse + { + }; + } +} \ No newline at end of file diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcWorkerConnection.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcWorkerConnection.cs new file mode 100644 index 000000000000..8debecfe90dd --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcWorkerConnection.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GrpcWorkerConnection.cs + +using System.Threading.Channels; +using Grpc.Core; +using Microsoft.AutoGen.Protobuf; + +namespace Microsoft.AutoGen.Core.Grpc.Tests; + +internal sealed class GrpcWorkerConnection : IAsyncDisposable +{ + private static long s_nextConnectionId; + private Task _readTask = Task.CompletedTask; + private Task _writeTask = Task.CompletedTask; + private readonly string _connectionId = Interlocked.Increment(ref s_nextConnectionId).ToString(); + private readonly object _lock = new(); + private readonly HashSet _supportedTypes = []; + private readonly CancellationTokenSource _shutdownCancellationToken = new(); + public Task Completion { get; private set; } = Task.CompletedTask; + public IAsyncStreamReader RequestStream { get; } + public IServerStreamWriter ResponseStream { get; } + public ServerCallContext ServerCallContext { get; } + private readonly Channel _outboundMessages; + public GrpcWorkerConnection(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + RequestStream = requestStream; + ResponseStream = responseStream; + ServerCallContext = context; + _outboundMessages = Channel.CreateUnbounded(new UnboundedChannelOptions { AllowSynchronousContinuations = true, SingleReader = true, SingleWriter = false }); + } + public Task Connect() + { + var didSuppress = false; + if (!ExecutionContext.IsFlowSuppressed()) + { + didSuppress = true; + ExecutionContext.SuppressFlow(); + } + + try + { + _readTask = Task.Run(RunReadPump); + _writeTask = Task.Run(RunWritePump); + } + finally + { + if (didSuppress) + { + ExecutionContext.RestoreFlow(); + } + } + + return Completion = Task.WhenAll(_readTask, _writeTask); + } + public void AddSupportedType(string type) + { + lock (_lock) + { + _supportedTypes.Add(type); + } + } + public HashSet GetSupportedTypes() + { + lock (_lock) + { + return new HashSet(_supportedTypes); + } + } + public async Task SendMessage(Message message) + { + await _outboundMessages.Writer.WriteAsync(message).ConfigureAwait(false); + } + public async Task RunReadPump() + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + try + { + await foreach (var message in RequestStream.ReadAllAsync(_shutdownCancellationToken.Token)) + { + // Fire and forget + //_gateway.OnReceivedMessageAsync(this, message, _shutdownCancellationToken.Token).Ignore(); + } + } + catch (OperationCanceledException) + { + } + finally + { + _shutdownCancellationToken.Cancel(); + //_gateway.OnRemoveWorkerProcess(this); + } + } + + public async Task RunWritePump() + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + try + { + await foreach (var message in _outboundMessages.Reader.ReadAllAsync(_shutdownCancellationToken.Token)) + { + await ResponseStream.WriteAsync(message); + } + } + catch (OperationCanceledException) + { + } + finally + { + _shutdownCancellationToken.Cancel(); + } + } + + public async ValueTask DisposeAsync() + { + _shutdownCancellationToken.Cancel(); + await Completion.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + + public override string ToString() => $"Connection-{_connectionId}"; +} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj index f14497e75fbc..8c47b5326094 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj @@ -10,7 +10,7 @@ - + From ef9230eb6d3e21e92515087e0d3d28ab22fd5cb6 Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Mon, 3 Feb 2025 22:26:52 -0800 Subject: [PATCH 14/25] moving all the tests to the right runtime --- .../AgentGrpcTests.cs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs index 4a8507822d7d..12f40c4dac51 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -44,16 +44,16 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => [Fact] public async Task AgentShouldReceiveMessagesWhenSubscribedTest() { - var runtime = new InProcessRuntime(); - await runtime.StartAsync(); + var fixture = new GrpcAgentRuntimeFixture(); + var runtime = (GrpcAgentRuntime)await fixture.Start(); Logger logger = new(new LoggerFactory()); SubscribedAgent agent = null!; - await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => + await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => { agent = new SubscribedAgent(id, runtime, logger); - return ValueTask.FromResult(agent); + return await ValueTask.FromResult(agent); }); // Ensure the agent is actually created @@ -68,8 +68,6 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => await runtime.PublishMessageAsync(new Core.Tests.TextMessage { Source = topicType, Content = "test" }, new TopicId(topicType)).ConfigureAwait(true); - await runtime.RunUntilIdleAsync(); - agent.ReceivedMessages.Any().Should().BeTrue("Agent should receive messages when subscribed."); } @@ -77,11 +75,11 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => public async Task SendMessageAsyncShouldReturnResponseTest() { // Arrange - var runtime = new InProcessRuntime(); - await runtime.StartAsync(); + var fixture = new GrpcAgentRuntimeFixture(); + var runtime = (GrpcAgentRuntime)await fixture.Start(); Logger logger = new(new LoggerFactory()); - await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => ValueTask.FromResult(new TestAgent(id, runtime, logger))); + await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => await ValueTask.FromResult(new TestAgent(id, runtime, logger))); await runtime.RegisterImplicitAgentSubscriptionsAsync("MyAgent"); var agentId = new AgentId("MyAgent", "TestAgent"); @@ -113,13 +111,13 @@ public ValueTask HandleAsync(string item, MessageContext messageContext) [Fact] public async Task SubscribeAsyncRemoveSubscriptionAsyncAndGetSubscriptionsTest() { - var runtime = new InProcessRuntime(); - await runtime.StartAsync(); + var fixture = new GrpcAgentRuntimeFixture(); + var runtime = (GrpcAgentRuntime)await fixture.Start(); ReceiverAgent? agent = null; - await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => + await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => { agent = new ReceiverAgent(id, runtime); - return ValueTask.FromResult(agent); + return await ValueTask.FromResult(agent); }); Assert.Null(agent); @@ -152,8 +150,9 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => [Fact] public async Task AgentShouldSaveStateCorrectlyTest() { - var runtime = new InProcessRuntime(); - await runtime.StartAsync(); + + var fixture = new GrpcAgentRuntimeFixture(); + var runtime = (GrpcAgentRuntime)await fixture.Start(); Logger logger = new(new LoggerFactory()); TestAgent agent = new TestAgent(new AgentId("TestType", "TestKey"), runtime, logger); From 05a077bf33c90a1c5f9af160094e7050d3a1c379 Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Mon, 3 Feb 2025 22:28:31 -0800 Subject: [PATCH 15/25] cleanup each test --- .../test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs index 12f40c4dac51..b7ed874f66ba 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -69,6 +69,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => await runtime.PublishMessageAsync(new Core.Tests.TextMessage { Source = topicType, Content = "test" }, new TopicId(topicType)).ConfigureAwait(true); agent.ReceivedMessages.Any().Should().BeTrue("Agent should receive messages when subscribed."); + fixture.Dispose(); } [Fact] @@ -93,6 +94,7 @@ public async Task SendMessageAsyncShouldReturnResponseTest() { Assert.Equal("Request", responseString); } + fixture.Dispose(); } public class ReceiverAgent(AgentId id, @@ -145,6 +147,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => await Task.Delay(100); Assert.True(agent.ReceivedItems.Count == 1); + fixture.Dispose(); } [Fact] @@ -167,5 +170,6 @@ public async Task AgentShouldSaveStateCorrectlyTest() // Add a sample value and verify it updates correctly state["testKey"] = "testValue"; state.Should().ContainKey("testKey").WhoseValue.Should().Be("testValue"); + fixture.Dispose(); } } From 74329098768ad6d4a7c09597243e9f893c8975d7 Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Tue, 4 Feb 2025 12:00:27 -0800 Subject: [PATCH 16/25] Simplify test server side --- .../AgentGrpcTests.cs | 4 +- .../GrpcAgentServiceFixture.cs | 46 ++++--------------- ...nection.cs => TestGrpcWorkerConnection.cs} | 22 +++++++-- .../AgentTests.cs | 2 +- 4 files changed, 31 insertions(+), 43 deletions(-) rename dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/{GrpcWorkerConnection.cs => TestGrpcWorkerConnection.cs} (75%) diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs index b7ed874f66ba..61c604b33eb4 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -35,7 +35,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => var topicType = "TestTopic"; - await runtime.PublishMessageAsync(new Core.Tests.TextMessage { Source = topicType, Content = "test" }, new TopicId(topicType)).ConfigureAwait(true); + await runtime.PublishMessageAsync(new Contracts.TextMessage { Source = topicType, TextMessage_ = "test" }, new TopicId(topicType)).ConfigureAwait(true); agent.ReceivedMessages.Any().Should().BeFalse("Agent should not receive messages when not subscribed."); fixture.Dispose(); @@ -66,7 +66,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => var topicType = "TestTopic"; - await runtime.PublishMessageAsync(new Core.Tests.TextMessage { Source = topicType, Content = "test" }, new TopicId(topicType)).ConfigureAwait(true); + await runtime.PublishMessageAsync(new Contracts.TextMessage { Source = topicType, TextMessage_ = "test" }, new TopicId(topicType)).ConfigureAwait(true); agent.ReceivedMessages.Any().Should().BeTrue("Agent should receive messages when subscribed."); fixture.Dispose(); diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs index 83032d5026a8..75eba7af47a7 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs @@ -4,14 +4,16 @@ using Microsoft.AutoGen.Protobuf; namespace Microsoft.AutoGen.Core.Grpc.Tests; +/// +/// This fixture is largely just a loopback as we are testing the client side logic of the GrpcAgentRuntime in isolation from the rest of the system. +/// public sealed class GrpcAgentServiceFixture() : AgentRpc.AgentRpcBase { - public override async Task OpenChannel(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) { try { - var workerProcess = new GrpcWorkerConnection(requestStream, responseStream, context); + var workerProcess = new TestGrpcWorkerConnection(requestStream, responseStream, context); await workerProcess.Connect().ConfigureAwait(true); } catch @@ -23,38 +25,10 @@ public override async Task OpenChannel(IAsyncStreamReader requestStream throw; } } - public override async Task GetState(AgentId request, ServerCallContext context) - { - return new GetStateResponse { AgentState = new AgentState { AgentId = request } }; - } - public override async Task SaveState(AgentState request, ServerCallContext context) - { - return new SaveStateResponse - { - }; - } - public override async Task AddSubscription(AddSubscriptionRequest request, ServerCallContext context) - { - return new AddSubscriptionResponse - { - }; - } - public override async Task RemoveSubscription(RemoveSubscriptionRequest request, ServerCallContext context) - { - return new RemoveSubscriptionResponse - { - }; - } - public override async Task GetSubscriptions(GetSubscriptionsRequest request, ServerCallContext context) - { - return new GetSubscriptionsResponse - { - }; - } - public override async Task RegisterAgent(RegisterAgentTypeRequest request, ServerCallContext context) - { - return new RegisterAgentTypeResponse - { - }; - } + public override async Task GetState(Protobuf.AgentId request, ServerCallContext context) => new GetStateResponse { AgentState = new AgentState { AgentId = request } }; + public override async Task SaveState(AgentState request, ServerCallContext context) => new SaveStateResponse { }; + public override async Task AddSubscription(AddSubscriptionRequest request, ServerCallContext context) => new AddSubscriptionResponse { }; + public override async Task RemoveSubscription(RemoveSubscriptionRequest request, ServerCallContext context) => new RemoveSubscriptionResponse { }; + public override async Task GetSubscriptions(GetSubscriptionsRequest request, ServerCallContext context) => new GetSubscriptionsResponse { }; + public override async Task RegisterAgent(RegisterAgentTypeRequest request, ServerCallContext context) => new RegisterAgentTypeResponse { }; } \ No newline at end of file diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcWorkerConnection.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/TestGrpcWorkerConnection.cs similarity index 75% rename from dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcWorkerConnection.cs rename to dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/TestGrpcWorkerConnection.cs index 8debecfe90dd..20b8169db11f 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcWorkerConnection.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/TestGrpcWorkerConnection.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// GrpcWorkerConnection.cs +// TestGrpcWorkerConnection.cs using System.Threading.Channels; using Grpc.Core; @@ -7,7 +7,7 @@ namespace Microsoft.AutoGen.Core.Grpc.Tests; -internal sealed class GrpcWorkerConnection : IAsyncDisposable +internal sealed class TestGrpcWorkerConnection : IAsyncDisposable { private static long s_nextConnectionId; private Task _readTask = Task.CompletedTask; @@ -21,7 +21,7 @@ internal sealed class GrpcWorkerConnection : IAsyncDisposable public IServerStreamWriter ResponseStream { get; } public ServerCallContext ServerCallContext { get; } private readonly Channel _outboundMessages; - public GrpcWorkerConnection(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + public TestGrpcWorkerConnection(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) { RequestStream = requestStream; ResponseStream = responseStream; @@ -77,8 +77,22 @@ public async Task RunReadPump() { await foreach (var message in RequestStream.ReadAllAsync(_shutdownCancellationToken.Token)) { - // Fire and forget //_gateway.OnReceivedMessageAsync(this, message, _shutdownCancellationToken.Token).Ignore(); + switch (message.MessageCase) + { + case Message.MessageOneofCase.Request: + await SendMessage(new Message { Request = message.Request }).ConfigureAwait(false); + break; + case Message.MessageOneofCase.Response: + await SendMessage(new Message { Response = message.Response }).ConfigureAwait(false); + break; + case Message.MessageOneofCase.CloudEvent: + await SendMessage(new Message { CloudEvent = message.CloudEvent }).ConfigureAwait(false); + break; + default: + // if it wasn't recognized return bad request + throw new RpcException(new Status(StatusCode.InvalidArgument, $"Unknown message type for message '{message}'")); + }; } } catch (OperationCanceledException) diff --git a/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs index cc39b3564c66..c091f9eb7478 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Tests/AgentTests.cs @@ -109,7 +109,7 @@ public ValueTask HandleAsync(string item, MessageContext messageContext) } [Fact] - public async Task SubscribeAsyncRemoveSubscriptionAsyncAndGetSubscriptionsTest() + public async Task SubscribeAsyncRemoveSubscriptionAsyncTest() { var runtime = new InProcessRuntime(); await runtime.StartAsync(); From 5f4748f7acc78a8c2d375724296f23a2f1a895f4 Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 09:45:14 -0500 Subject: [PATCH 17/25] finish tests remove protos --- .../Core.Grpc/GrpcAgentRuntime.cs | 8 +-- .../Microsoft.AutoGen.Core.Grpc.csproj | 1 - .../AgentGrpcTests.cs | 71 +++++++------------ .../GrpcAgentRuntimeFixture.cs | 6 +- .../GrpcAgentServiceFixture.cs | 6 +- .../Microsoft.AutoGen.Core.Grpc.Tests.csproj | 11 ++- .../TestProtobufAgent.cs | 50 +++++++++++++ .../messages.proto | 13 ++++ protos/agent_events.proto | 43 ----------- protos/agent_states.proto | 8 --- 10 files changed, 105 insertions(+), 112 deletions(-) create mode 100644 dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/TestProtobufAgent.cs create mode 100644 dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/messages.proto delete mode 100644 protos/agent_events.proto delete mode 100644 protos/agent_states.proto diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs index 2f2057f6aa62..1ff1036016d1 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentRuntime.cs @@ -144,10 +144,6 @@ private async ValueTask HandleRequest(RpcRequest request, CancellationToken canc { throw new InvalidOperationException("Target is null."); } - if (request.Source is null) - { - throw new InvalidOperationException("Source is null."); - } var agentId = request.Target; var agent = await this._agentsContainer.EnsureAgentAsync(agentId.FromProtobuf()); @@ -158,7 +154,7 @@ private async ValueTask HandleRequest(RpcRequest request, CancellationToken canc var messageContext = new MessageContext(request.RequestId, cancellationToken) { - Sender = request.Source.FromProtobuf(), + Sender = request.Source?.FromProtobuf() ?? null, Topic = null, IsRpc = true }; @@ -278,7 +274,7 @@ public Task StopAsync(CancellationToken cancellationToken) var request = new RpcRequest { RequestId = Guid.NewGuid().ToString(), - Source = (sender ?? new Contracts.AgentId()).ToProtobuf(), + Source = sender?.ToProtobuf() ?? null, Target = recepient.ToProtobuf(), Payload = payload, }; diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj b/dotnet/src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj index c28a9b1c9087..6a68de1d8903 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/Microsoft.AutoGen.Core.Grpc.csproj @@ -14,7 +14,6 @@ - diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs index 61c604b33eb4..e15336152a67 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -2,9 +2,9 @@ // AgentGrpcTests.cs using FluentAssertions; using Microsoft.AutoGen.Contracts; +// using Microsoft.AutoGen.Core.Tests; +using Microsoft.AutoGen.Core.Grpc.Tests.Protobuf; using Microsoft.Extensions.Logging; -using Microsoft.AutoGen.Core.Tests; - using Xunit; namespace Microsoft.AutoGen.Core.Grpc.Tests; @@ -19,11 +19,11 @@ public async Task AgentShouldNotReceiveMessagesWhenNotSubscribedTest() var runtime = (GrpcAgentRuntime)await fixture.Start(); Logger logger = new(new LoggerFactory()); - TestAgent agent = null!; + TestProtobufAgent agent = null!; await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => { - agent = new TestAgent(id, runtime, logger); + agent = new TestProtobufAgent(id, runtime, logger); return await ValueTask.FromResult(agent); }); @@ -35,7 +35,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => var topicType = "TestTopic"; - await runtime.PublishMessageAsync(new Contracts.TextMessage { Source = topicType, TextMessage_ = "test" }, new TopicId(topicType)).ConfigureAwait(true); + await runtime.PublishMessageAsync(new Protobuf.TextMessage { Source = topicType, Content = "test" }, new TopicId(topicType)).ConfigureAwait(true); agent.ReceivedMessages.Any().Should().BeFalse("Agent should not receive messages when not subscribed."); fixture.Dispose(); @@ -48,11 +48,11 @@ public async Task AgentShouldReceiveMessagesWhenSubscribedTest() var runtime = (GrpcAgentRuntime)await fixture.Start(); Logger logger = new(new LoggerFactory()); - SubscribedAgent agent = null!; + SubscribedProtobufAgent agent = null!; await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => { - agent = new SubscribedAgent(id, runtime, logger); + agent = new SubscribedProtobufAgent(id, runtime, logger); return await ValueTask.FromResult(agent); }); @@ -62,11 +62,14 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => // Validate agent ID agentId.Should().Be(agent.Id, "Agent ID should match the registered agent"); - await runtime.RegisterImplicitAgentSubscriptionsAsync("MyAgent"); + await runtime.RegisterImplicitAgentSubscriptionsAsync("MyAgent"); var topicType = "TestTopic"; - await runtime.PublishMessageAsync(new Contracts.TextMessage { Source = topicType, TextMessage_ = "test" }, new TopicId(topicType)).ConfigureAwait(true); + await runtime.PublishMessageAsync(new TextMessage { Source = topicType, Content = "test" }, new TopicId(topicType)).ConfigureAwait(true); + + // Wait for the message to be processed + await Task.Delay(100); agent.ReceivedMessages.Any().Should().BeTrue("Agent should receive messages when subscribed."); fixture.Dispose(); @@ -80,30 +83,27 @@ public async Task SendMessageAsyncShouldReturnResponseTest() var runtime = (GrpcAgentRuntime)await fixture.Start(); Logger logger = new(new LoggerFactory()); - await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => await ValueTask.FromResult(new TestAgent(id, runtime, logger))); - await runtime.RegisterImplicitAgentSubscriptionsAsync("MyAgent"); - - var agentId = new AgentId("MyAgent", "TestAgent"); - + await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => await ValueTask.FromResult(new TestProtobufAgent(id, runtime, logger))); + var agentId = new AgentId("MyAgent", "default"); var response = await runtime.SendMessageAsync(new RpcTextMessage { Source = "TestTopic", Content = "Request" }, agentId); // Assert Assert.NotNull(response); - Assert.IsType(response); - if (response is string responseString) + Assert.IsType(response); + if (response is RpcTextMessage responseString) { - Assert.Equal("Request", responseString); + Assert.Equal("Request", responseString.Content); } fixture.Dispose(); } public class ReceiverAgent(AgentId id, IAgentRuntime runtime) : BaseAgent(id, runtime, "Receiver Agent", null), - IHandle + IHandle { - public ValueTask HandleAsync(string item, MessageContext messageContext) + public ValueTask HandleAsync(TextMessage item, MessageContext messageContext) { - ReceivedItems.Add(item); + ReceivedItems.Add(item.Content); return ValueTask.CompletedTask; } @@ -128,7 +128,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => Assert.True(agent.ReceivedItems.Count == 0); var topicTypeName = "TestTopic"; - await runtime.PublishMessageAsync("info", new TopicId(topicTypeName)); + await runtime.PublishMessageAsync(new TextMessage { Source = "topic", Content = "test" }, new TopicId(topicTypeName)); await Task.Delay(100); Assert.True(agent.ReceivedItems.Count == 0); @@ -136,40 +136,17 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => var subscription = new TypeSubscription(topicTypeName, "MyAgent"); await runtime.AddSubscriptionAsync(subscription); - await runtime.PublishMessageAsync("info", new TopicId(topicTypeName)); + await runtime.PublishMessageAsync(new TextMessage { Source = "topic", Content = "test" }, new TopicId(topicTypeName)); await Task.Delay(100); Assert.True(agent.ReceivedItems.Count == 1); - Assert.Equal("info", agent.ReceivedItems[0]); + Assert.Equal("test", agent.ReceivedItems[0]); await runtime.RemoveSubscriptionAsync(subscription.Id); - await runtime.PublishMessageAsync("info", new TopicId(topicTypeName)); + await runtime.PublishMessageAsync(new TextMessage { Source = "topic", Content = "test" }, new TopicId(topicTypeName)); await Task.Delay(100); Assert.True(agent.ReceivedItems.Count == 1); fixture.Dispose(); } - - [Fact] - public async Task AgentShouldSaveStateCorrectlyTest() - { - - var fixture = new GrpcAgentRuntimeFixture(); - var runtime = (GrpcAgentRuntime)await fixture.Start(); - - Logger logger = new(new LoggerFactory()); - TestAgent agent = new TestAgent(new AgentId("TestType", "TestKey"), runtime, logger); - - var state = await agent.SaveStateAsync(); - - // Ensure state is a dictionary - state.Should().NotBeNull(); - state.Should().BeOfType>(); - state.Should().BeEmpty("Default SaveStateAsync should return an empty dictionary."); - - // Add a sample value and verify it updates correctly - state["testKey"] = "testValue"; - state.Should().ContainKey("testKey").WhoseValue.Should().Be("testValue"); - fixture.Dispose(); - } } diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs index f9adaf0659b7..bade7f785757 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs @@ -2,7 +2,7 @@ // GrpcAgentRuntimeFixture.cs using Microsoft.AspNetCore.Builder; using Microsoft.AutoGen.Contracts; -using Microsoft.AutoGen.Core.Tests; +// using Microsoft.AutoGen.Core.Tests; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -44,7 +44,7 @@ private static async Task ClientBuilder() { var appBuilder = new AgentsAppBuilder(); appBuilder.AddGrpcAgentWorker(); - appBuilder.AddAgent("TestAgent"); + appBuilder.AddAgent("TestAgent"); return await appBuilder.BuildAsync(); } private static async Task ServerBuilder() @@ -80,4 +80,4 @@ public void Dispose() Stop(); } -} \ No newline at end of file +} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs index 75eba7af47a7..98c47764269d 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentServiceFixture.cs @@ -5,7 +5,7 @@ namespace Microsoft.AutoGen.Core.Grpc.Tests; /// -/// This fixture is largely just a loopback as we are testing the client side logic of the GrpcAgentRuntime in isolation from the rest of the system. +/// This fixture is largely just a loopback as we are testing the client side logic of the GrpcAgentRuntime in isolation from the rest of the system. /// public sealed class GrpcAgentServiceFixture() : AgentRpc.AgentRpcBase { @@ -25,10 +25,10 @@ public override async Task OpenChannel(IAsyncStreamReader requestStream throw; } } - public override async Task GetState(Protobuf.AgentId request, ServerCallContext context) => new GetStateResponse { AgentState = new AgentState { AgentId = request } }; + public override async Task GetState(AgentId request, ServerCallContext context) => new GetStateResponse { AgentState = new AgentState { AgentId = request } }; public override async Task SaveState(AgentState request, ServerCallContext context) => new SaveStateResponse { }; public override async Task AddSubscription(AddSubscriptionRequest request, ServerCallContext context) => new AddSubscriptionResponse { }; public override async Task RemoveSubscription(RemoveSubscriptionRequest request, ServerCallContext context) => new RemoveSubscriptionResponse { }; public override async Task GetSubscriptions(GetSubscriptionsRequest request, ServerCallContext context) => new GetSubscriptionsResponse { }; public override async Task RegisterAgent(RegisterAgentTypeRequest request, ServerCallContext context) => new RegisterAgentTypeResponse { }; -} \ No newline at end of file +} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj index 8c47b5326094..e3573c93451a 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/Microsoft.AutoGen.Core.Grpc.Tests.csproj @@ -10,8 +10,17 @@ - + + + + + + + + + + diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/TestProtobufAgent.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/TestProtobufAgent.cs new file mode 100644 index 000000000000..6f5ad4aa9e5b --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/TestProtobufAgent.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// TestProtobufAgent.cs + +using Microsoft.AutoGen.Contracts; +using Microsoft.AutoGen.Core.Grpc.Tests.Protobuf; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AutoGen.Core.Grpc.Tests; + +/// +/// The test agent is a simple agent that is used for testing purposes. +/// +public class TestProtobufAgent(AgentId id, + IAgentRuntime runtime, + Logger? logger = null) : BaseAgent(id, runtime, "Test Agent", logger), + IHandle, + IHandle + +{ + public ValueTask HandleAsync(TextMessage item, MessageContext messageContext) + { + ReceivedMessages[item.Source] = item.Content; + return ValueTask.CompletedTask; + } + + public ValueTask HandleAsync(RpcTextMessage item, MessageContext messageContext) + { + ReceivedMessages[item.Source] = item.Content; + return ValueTask.FromResult(new RpcTextMessage { Source = item.Source, Content = item.Content }); + } + + public List ReceivedItems { get; private set; } = []; + + /// + /// Key: source + /// Value: message + /// + private readonly Dictionary _receivedMessages = new(); + public Dictionary ReceivedMessages => _receivedMessages; +} + +[TypeSubscription("TestTopic")] +public class SubscribedProtobufAgent : TestProtobufAgent +{ + public SubscribedProtobufAgent(AgentId id, + IAgentRuntime runtime, + Logger? logger = null) : base(id, runtime, logger) + { + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/messages.proto b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/messages.proto new file mode 100644 index 000000000000..7f2c275e691f --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/messages.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +option csharp_namespace = "Microsoft.AutoGen.Core.Grpc.Tests.Protobuf"; + +message TextMessage { + string content = 1; + string source = 2; +} + +message RpcTextMessage { + string content = 1; + string source = 2; +} \ No newline at end of file diff --git a/protos/agent_events.proto b/protos/agent_events.proto deleted file mode 100644 index a97df6e5855f..000000000000 --- a/protos/agent_events.proto +++ /dev/null @@ -1,43 +0,0 @@ -syntax = "proto3"; - -package agents; - -option csharp_namespace = "Microsoft.AutoGen.Contracts"; -message TextMessage { - string textMessage = 1; - string source = 2; -} -message Input { - string message = 1; -} -message InputProcessed { - string route = 1; -} -message Output { - string message = 1; -} -message OutputWritten { - string route = 1; -} -message IOError { - string message = 1; -} -message NewMessageReceived { - string message = 1; -} -message ResponseGenerated { - string response = 1; -} -message GoodBye { - string message = 1; -} -message MessageStored { - string message = 1; -} -message ConversationClosed { - string user_id = 1; - string user_message = 2; -} -message Shutdown { - string message = 1; -} diff --git a/protos/agent_states.proto b/protos/agent_states.proto deleted file mode 100644 index 945772861cc8..000000000000 --- a/protos/agent_states.proto +++ /dev/null @@ -1,8 +0,0 @@ -syntax = "proto3"; -package agents; - -option csharp_namespace = "Microsoft.AutoGen.Contracts"; - -message AgentState { - string message = 1; -} From 2cd30eb59380fd7f3104945e9b0e1a589ec81c4a Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 10:00:28 -0500 Subject: [PATCH 18/25] dont use ssl for loopback test --- .../GrpcAgentRuntimeFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs index bade7f785757..d95e3a14df38 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs @@ -29,7 +29,7 @@ public async Task Start(bool initialize = true) // Update environment variables so each test runs independently Environment.SetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS", port.ToString()); - Environment.SetEnvironmentVariable("AGENT_HOST", $"https://localhost:{port}"); + Environment.SetEnvironmentVariable("AGENT_HOST", $"http://localhost:{port}"); Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); Server = ServerBuilder().Result; await Server.StartAsync().ConfigureAwait(true); From 8940f2ee492d5ed6f60cae67af054d97f8e653a2 Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 10:27:24 -0500 Subject: [PATCH 19/25] go back to https --- .../GrpcAgentRuntimeFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs index d95e3a14df38..bade7f785757 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/GrpcAgentRuntimeFixture.cs @@ -29,7 +29,7 @@ public async Task Start(bool initialize = true) // Update environment variables so each test runs independently Environment.SetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS", port.ToString()); - Environment.SetEnvironmentVariable("AGENT_HOST", $"http://localhost:{port}"); + Environment.SetEnvironmentVariable("AGENT_HOST", $"https://localhost:{port}"); Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); Server = ServerBuilder().Result; await Server.StartAsync().ConfigureAwait(true); From 0e614ef2b61e98345eaaab32f7643963d62b495e Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 10:31:09 -0500 Subject: [PATCH 20/25] try trusting dev cert --- .github/workflows/dotnet-build.yml | 3 +++ .github/workflows/dotnet-release.yml | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 1da80b327d26..370db4332834 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -104,6 +104,7 @@ jobs: dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - name: Unit Test V1 run: dotnet test --no-build -bl --configuration Release --filter "Category=UnitV1" + - run: dotnet dev-certs https --trust - name: Unit Test V2 run: dotnet test --no-build -bl --configuration Release --filter "Category=UnitV2" @@ -156,6 +157,7 @@ jobs: run: | echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true + - run: dotnet dev-certs https --trust - name: Integration Test run: dotnet --version && dotnet test --no-build -bl --configuration Release --filter "Category=Integration" - name: Restore the global.json @@ -231,6 +233,7 @@ jobs: run: | echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true + - run: dotnet dev-certs https --trust - name: OpenAI Test run: dotnet test --no-build -bl --configuration Release --filter type!=integration env: diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index 23f4258a0e0c..a09eb7e68843 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -52,6 +52,7 @@ jobs: run: | echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true + - run: dotnet dev-certs https --trust - name: Unit Test run: dotnet test --no-build -bl --configuration Release env: From 4b2fea3c43359b18f0ff7f48d2567450ed7b958b Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 10:37:54 -0500 Subject: [PATCH 21/25] try sudo --- .github/workflows/dotnet-build.yml | 6 +++--- .github/workflows/dotnet-release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 370db4332834..05a314aa6d46 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -104,7 +104,7 @@ jobs: dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - name: Unit Test V1 run: dotnet test --no-build -bl --configuration Release --filter "Category=UnitV1" - - run: dotnet dev-certs https --trust + - run: sudo dotnet dev-certs https --trust --no-password - name: Unit Test V2 run: dotnet test --no-build -bl --configuration Release --filter "Category=UnitV2" @@ -157,7 +157,7 @@ jobs: run: | echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - - run: dotnet dev-certs https --trust + - run: sudo dotnet dev-certs https --trust --no-password - name: Integration Test run: dotnet --version && dotnet test --no-build -bl --configuration Release --filter "Category=Integration" - name: Restore the global.json @@ -233,7 +233,7 @@ jobs: run: | echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - - run: dotnet dev-certs https --trust + - run: sudo dotnet dev-certs https --trust --no-password - name: OpenAI Test run: dotnet test --no-build -bl --configuration Release --filter type!=integration env: diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index a09eb7e68843..fa114267136c 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -52,7 +52,7 @@ jobs: run: | echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - - run: dotnet dev-certs https --trust + - run: sudo dotnet dev-certs https --trust --no-password - name: Unit Test run: dotnet test --no-build -bl --configuration Release env: From 583158068916f33bdf2d0e178c01bd830a01a58b Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 10:45:50 -0500 Subject: [PATCH 22/25] move --- .github/workflows/dotnet-build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 05a314aa6d46..3ac70848e6fd 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -89,6 +89,8 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' + - name: Install dev certs + run: dotnet --version && dotnet dev-certs https --trust - name: Restore dependencies run: | # dotnet nuget add source --name dotnet-tool https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json --configfile NuGet.config @@ -104,7 +106,6 @@ jobs: dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - name: Unit Test V1 run: dotnet test --no-build -bl --configuration Release --filter "Category=UnitV1" - - run: sudo dotnet dev-certs https --trust --no-password - name: Unit Test V2 run: dotnet test --no-build -bl --configuration Release --filter "Category=UnitV2" @@ -157,7 +158,6 @@ jobs: run: | echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - - run: sudo dotnet dev-certs https --trust --no-password - name: Integration Test run: dotnet --version && dotnet test --no-build -bl --configuration Release --filter "Category=Integration" - name: Restore the global.json @@ -226,6 +226,8 @@ jobs: with: dotnet-version: '8.0.x' global-json-file: dotnet/global.json + - name: Install dev certs + run: dotnet --version && dotnet dev-certs https --trust - name: Restore dependencies run: | dotnet restore -bl @@ -233,7 +235,6 @@ jobs: run: | echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - - run: sudo dotnet dev-certs https --trust --no-password - name: OpenAI Test run: dotnet test --no-build -bl --configuration Release --filter type!=integration env: From 5c7b1c83897b54e5afef20ba80f912a7b7311e51 Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 10:53:58 -0500 Subject: [PATCH 23/25] sudo devcert --- .github/workflows/dotnet-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 3ac70848e6fd..0ba4f18cc9ed 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -90,7 +90,7 @@ jobs: with: dotnet-version: '8.0.x' - name: Install dev certs - run: dotnet --version && dotnet dev-certs https --trust + run: dotnet --version && sudo dotnet dev-certs https --trust - name: Restore dependencies run: | # dotnet nuget add source --name dotnet-tool https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json --configfile NuGet.config From 0114b05a2620c60f595828e643f3976367b2a442 Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 11:09:12 -0500 Subject: [PATCH 24/25] move grpc tests --- .github/workflows/dotnet-build.yml | 39 ++++++++++++++++++- .../AgentGrpcTests.cs | 2 +- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 0ba4f18cc9ed..472c6cb17fc0 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -89,8 +89,6 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' - - name: Install dev certs - run: dotnet --version && sudo dotnet dev-certs https --trust - name: Restore dependencies run: | # dotnet nuget add source --name dotnet-tool https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json --configfile NuGet.config @@ -109,6 +107,43 @@ jobs: - name: Unit Test V2 run: dotnet test --no-build -bl --configuration Release --filter "Category=UnitV2" + grpc-unit-tests: + name: Dotnet Grpc unit tests + needs: paths-filter + if: needs.paths-filter.outputs.hasChanges == 'true' + defaults: + run: + working-directory: dotnet + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - name: Setup .NET 8.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + - name: Install dev certs + run: dotnet --version && dotnet dev-certs https --trust + - name: Restore dependencies + run: | + # dotnet nuget add source --name dotnet-tool https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json --configfile NuGet.config + dotnet restore -bl + - name: Format check + run: | + echo "Format check" + echo "If you see any error in this step, please run 'dotnet format' locally to format the code." + dotnet format --verify-no-changes -v diag --no-restore + - name: Build + run: dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true + - name: GRPC tests + run: dotnet test --no-build -bl --configuration Release --filter "Category=GRPC" + integration-test: strategy: fail-fast: true diff --git a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs index e15336152a67..a2a970bebb41 100644 --- a/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Core.Grpc.Tests/AgentGrpcTests.cs @@ -9,7 +9,7 @@ namespace Microsoft.AutoGen.Core.Grpc.Tests; -[Trait("Category", "UnitV2")] +[Trait("Category", "GRPC")] public class AgentGrpcTests { [Fact] From c95b35fc54d537de6d7982ea925cf7b4d374d5c2 Mon Sep 17 00:00:00 2001 From: Jack Gerrits Date: Wed, 5 Feb 2025 11:11:19 -0500 Subject: [PATCH 25/25] dont format --- .github/workflows/dotnet-build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 472c6cb17fc0..bf7570239d61 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -134,11 +134,6 @@ jobs: run: | # dotnet nuget add source --name dotnet-tool https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json --configfile NuGet.config dotnet restore -bl - - name: Format check - run: | - echo "Format check" - echo "If you see any error in this step, please run 'dotnet format' locally to format the code." - dotnet format --verify-no-changes -v diag --no-restore - name: Build run: dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - name: GRPC tests