// Copyright (c) Microsoft. All rights reserved. using System.Collections.Concurrent; using Dapr.Actors.Client; using Grpc.Core; using Microsoft.SemanticKernel; using Microsoft.VisualStudio.Threading; using ProcessWithCloudEvents.Grpc.Clients; using ProcessWithCloudEvents.Grpc.DocumentationGenerator; using ProcessWithCloudEvents.Processes; using ProcessWithCloudEvents.Processes.Models; namespace ProcessWithCloudEvents.Grpc.Services; /// /// This gRPC service handles the generation of documents using/invoking a SK Process /// public class DocumentGenerationService : GrpcDocumentationGeneration.GrpcDocumentationGenerationBase { private readonly ILogger _logger; private readonly Kernel _kernel; private readonly IActorProxyFactory _actorProxyFactory; private readonly ConcurrentDictionary>> _docReviewSubscribers; private readonly ConcurrentDictionary>> _publishDocumentSubscribers; /// /// Constructor for the /// /// /// /// public DocumentGenerationService(ILogger logger, Kernel kernel, IActorProxyFactory actorProxy) { this._logger = logger; this._kernel = kernel; this._actorProxyFactory = actorProxy; this._docReviewSubscribers = new(); this._publishDocumentSubscribers = new(); } /// /// Method that receives a request to generate documentation, this will start the SK process /// defined in
/// It will use the processId passed in the request or generate a new one if not provided ///
/// /// /// public override async Task UserRequestFeatureDocumentation(FeatureDocumentationRequest request, ServerCallContext context) { var processId = string.IsNullOrEmpty(request.ProcessId) ? Guid.NewGuid().ToString() : request.ProcessId; var process = DocumentGenerationProcess.CreateProcessBuilder().Build(); var processContext = await process.StartAsync(new KernelProcessEvent() { Id = DocumentGenerationProcess.DocGenerationEvents.StartDocumentGeneration, // The object ProductInfo is sent because this is the type the GatherProductInfoStep is expecting Data = new ProductInfo() { Title = request.Title, Content = request.Content, UserInput = request.UserDescription }, }, processId, this._actorProxyFactory); return new ProcessData { ProcessId = processId }; } /// /// Method that receives a request to request user review of documentation, this will send a request to the client /// if subscribed to the method previously with the same process id.
/// This method is meant to be used within the SK process from the implementation. ///
/// /// /// public override async Task RequestUserReviewDocumentationFromProcess(DocumentationContentRequest request, ServerCallContext context) { if (this._docReviewSubscribers.TryGetValue(request.ProcessData.ProcessId, out var subscribers)) { foreach (var subscriber in subscribers) { await subscriber.WriteAsync(request).ConfigureAwait(false); } } return new Empty(); } /// /// Method that receives request to receive user review of documentation.
/// This is meant to be used by the external client ///
/// /// /// /// public override async Task RequestUserReviewDocumentation(ProcessData request, IServerStreamWriter responseStream, ServerCallContext context) { var subscribers = this._docReviewSubscribers.GetOrAdd(request.ProcessId, []); subscribers.Add(responseStream); try { // Wait until the client disconnects await context.CancellationToken.WaitHandle.ToTask(); } finally { // Remove the subscriber when client disconnects #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. subscribers.TryTake(out responseStream); #pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. } } /// /// Method that receives a request to approve or reject documentation, this will send the response to the SK process. /// This is meant to be used by the external client. /// /// /// /// public override async Task UserReviewedDocumentation(DocumentationApprovalRequest request, ServerCallContext context) { var process = DocumentGenerationProcess.CreateProcessBuilder().Build(); KernelProcessEvent processEvent; if (request.DocumentationApproved) { processEvent = new() { Id = DocumentGenerationProcess.DocGenerationEvents.UserApprovedDocument, Data = true, }; } else { processEvent = new() { Id = DocumentGenerationProcess.DocGenerationEvents.UserRejectedDocument, Data = request.Reason, }; } var processContext = await process.StartAsync(processEvent, request.ProcessData.ProcessId); return new Empty(); } /// /// Method used to publish the generated documentation, this will send the documentation to the client /// if subscribed to the method with the same process id.
/// This method is meant to be used within the SK process from the implementation. ///
/// /// /// public override async Task PublishDocumentation(DocumentationContentRequest request, ServerCallContext context) { if (this._publishDocumentSubscribers.TryGetValue(request.ProcessData.ProcessId, out var subscribers)) { foreach (var subscriber in subscribers) { await subscriber.WriteAsync(request).ConfigureAwait(false); } } return new Empty(); } /// /// Method that receives request to receive published documentation from a specific process id. /// This is meant to be used by the external client. /// /// /// /// /// public override async Task ReceivePublishedDocumentation(ProcessData request, IServerStreamWriter responseStream, ServerCallContext context) { var subscribers = this._publishDocumentSubscribers.GetOrAdd(request.ProcessId, []); subscribers.Add(responseStream); try { // Wait until the client disconnects await context.CancellationToken.WaitHandle.ToTask(); } finally { // Remove the subscriber when client disconnects #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. subscribers.TryTake(out responseStream); #pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. } } }