chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using Grpc.Net.Client;
using Microsoft.SemanticKernel;
using ProcessWithCloudEvents.Grpc.DocumentationGenerator;
using ProcessWithCloudEvents.Processes;
using ProcessWithCloudEvents.Processes.Models;
namespace ProcessWithCloudEvents.Grpc.Clients;
/// <summary>
/// Client that implements the <see cref="IExternalKernelProcessMessageChannel"/> interface used internally by the SK process
/// to emit events to external systems.<br/>
/// This implementation is an example of a gRPC client that emits events to a gRPC server
/// </summary>
public class DocumentGenerationGrpcClient : IExternalKernelProcessMessageChannel
{
private GrpcChannel? _grpcChannel;
private GrpcDocumentationGeneration.GrpcDocumentationGenerationClient? _grpcClient;
/// <inheritdoc/>
public async ValueTask Initialize()
{
this._grpcChannel = GrpcChannel.ForAddress("http://localhost:58641");
this._grpcClient = new GrpcDocumentationGeneration.GrpcDocumentationGenerationClient(this._grpcChannel);
}
/// <inheritdoc/>
public async ValueTask Uninitialize()
{
if (this._grpcChannel != null)
{
await this._grpcChannel.ShutdownAsync();
}
}
/// <inheritdoc/>
public async Task EmitExternalEventAsync(string externalTopicEvent, KernelProcessProxyMessage message)
{
if (this._grpcClient != null && message.EventData != null)
{
switch (externalTopicEvent)
{
case DocumentGenerationProcess.DocGenerationTopics.RequestUserReview:
var requestDocument = message.EventData.ToObject() as DocumentInfo;
if (requestDocument != null)
{
await this._grpcClient.RequestUserReviewDocumentationFromProcessAsync(new()
{
Title = requestDocument.Title,
AssistantMessage = "Document ready for user revision. Approve or reject document",
Content = requestDocument.Content,
ProcessData = new() { ProcessId = message.ProcessId }
});
}
return;
case DocumentGenerationProcess.DocGenerationTopics.PublishDocumentation:
var publishedDocument = message.EventData.ToObject() as DocumentInfo;
if (publishedDocument != null)
{
await this._grpcClient.PublishDocumentationAsync(new()
{
Title = publishedDocument.Title,
AssistantMessage = "Published Document Ready",
Content = publishedDocument.Content,
ProcessData = new() { ProcessId = message.ProcessId }
});
}
return;
}
}
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace ProcessWithCloudEvents.Grpc.Extensions;
/// <summary>
/// Class with extension methods for app configuration.
/// </summary>
public static class ConfigurationExtensions
{
/// <summary>
/// Returns <typeparamref name="TOptions"/> if it's valid or throws <see cref="ValidationException"/>.
/// </summary>
public static TOptions GetValid<TOptions>(this IConfigurationRoot configurationRoot, string sectionName)
{
var options = configurationRoot.GetSection(sectionName).Get<TOptions>()!;
Validator.ValidateObject(options, new(options));
return options;
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace ProcessWithCloudEvents.Grpc.Options;
/// <summary>
/// Configuration for OpenAI chat completion service.
/// </summary>
public class OpenAIOptions
{
public const string SectionName = "OpenAI";
[Required]
public string ChatModelId { get; set; } = string.Empty;
[Required]
public string ApiKey { get; set; } = string.Empty;
}
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>
$(NoWarn);CA2007,CS1591,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0110
</NoWarn>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\documentationGenerator.proto" GrpcServices="Both" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Experimental\Process.Abstractions\Process.Abstractions.csproj" />
<ProjectReference Include="..\..\..\..\src\Experimental\Process.Core\Process.Core.csproj" />
<ProjectReference Include="..\..\..\..\src\Experimental\Process.Runtime.Dapr\Process.Runtime.Dapr.csproj" />
<ProjectReference Include="..\ProcessWithCloudEvents.Processes\ProcessWithCloudEvents.Processes.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapr.Actors" />
<PackageReference Include="Dapr.Actors.AspNetCore" />
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.AspNetCore" />
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" />
<PackageReference Include="Grpc.AspNetCore.Web" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Grpc.Tools">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Media\" />
</ItemGroup>
</Project>
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using ProcessWithCloudEvents.Grpc.Clients;
using ProcessWithCloudEvents.Grpc.Extensions;
using ProcessWithCloudEvents.Grpc.Options;
using ProcessWithCloudEvents.Grpc.Services;
var builder = WebApplication.CreateBuilder(args);
var config = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.AddEnvironmentVariables()
.Build();
// Configure logging
builder.Services.AddLogging((logging) =>
{
logging.AddConsole();
logging.AddDebug();
});
var openAIOptions = config.GetValid<OpenAIOptions>(OpenAIOptions.SectionName);
// Configure the Kernel with DI. This is required for dependency injection to work with processes.
builder.Services.AddKernel();
builder.Services.AddOpenAIChatCompletion(openAIOptions.ChatModelId, openAIOptions.ApiKey);
builder.Services.AddSingleton<DocumentGenerationService>();
// Injecting SK Process custom grpc client IExternalKernelProcessMessageChannel implementation
builder.Services.AddSingleton<IExternalKernelProcessMessageChannel, DocumentGenerationGrpcClient>();
// Configure Dapr
builder.Services.AddActors(static options =>
{
// Register the actors required to run Processes
options.AddProcessActors();
});
// Enabling CORS for grpc-web when using webApp as client, remove if not needed
builder.Services.AddCors(o => o.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
// Add grpc related services.
builder.Services.AddGrpc();
builder.Services.AddGrpcReflection();
var app = builder.Build();
app.UseCors();
// Grpc services mapping
// Enabling grpc-web, remove if not needed
app.UseGrpcWeb();
// Enabling CORS for grpc-web, remove if not needed
app.MapGrpcReflectionService().RequireCors("AllowAll");
app.MapGrpcService<DocumentGenerationService>().EnableGrpcWeb().RequireCors("AllowAll");
// Dapr actors related mapping
app.MapActorsHandlers();
app.Run();
@@ -0,0 +1,38 @@
syntax = "proto3";
option csharp_namespace = "ProcessWithCloudEvents.Grpc.DocumentationGenerator";
service GrpcDocumentationGeneration {
rpc UserRequestFeatureDocumentation (FeatureDocumentationRequest) returns (ProcessData);
rpc RequestUserReviewDocumentationFromProcess (DocumentationContentRequest) returns (Empty);
rpc RequestUserReviewDocumentation (ProcessData) returns (stream DocumentationContentRequest);
rpc UserReviewedDocumentation (DocumentationApprovalRequest) returns (Empty);
rpc PublishDocumentation (DocumentationContentRequest) returns (Empty);
rpc ReceivePublishedDocumentation (ProcessData) returns (stream DocumentationContentRequest);
}
message FeatureDocumentationRequest {
string title = 1;
string userDescription = 2;
string content = 3;
string processId = 10;
}
message DocumentationContentRequest {
string title = 1;
string content = 2;
string assistantMessage = 3;
ProcessData processData = 10;
}
message DocumentationApprovalRequest {
bool documentationApproved = 1;
string reason = 2;
ProcessData processData = 10;
}
message ProcessData {
string processId = 1;
}
message Empty {}
@@ -0,0 +1,131 @@
# Process With Cloud Events - using gRPC
For using gRPC, this demo follows the guidelines suggested for any [gRPC ASP.NET Core App](https://learn.microsoft.com/en-us/aspnet/core/grpc/test-tools?view=aspnetcore-9.0).
Which for this demo means:
- Making use of `builder.Services.AddGrpcReflection()` and `app.MapGrpcReflectionService()`
- Making use of [`gRPCui`](https://github.com/fullstorydev/grpcui) for testing
## Explanation
This demo showcases how SK Process Framework could interact with a gRPC Server and clients.
The main difference of this demo is the custom implementation of the gRPC Server and client used internally by the SK Process in the SK Proxy Step.
Main gRPC components:
- `documentationGenerator.proto`: `<root>\dotnet\samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.Grpc\Protos\documentationGenerator.proto`
- gRPC Server: `<root>\dotnet\samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.Grpc\Services\DocumentGenerationService.cs`
- gRPC Client/IExternalKernelProcessMessageChannel implementation: `<root>\dotnet\samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.Grpc\Clients\DocumentGenerationGrpcClient.cs`
### SK Process and gRPC Events
``` mermaid
sequenceDiagram
participant grpcClient as gRPC Client
box Server
participant grpcServer as gRPC Server
participant SKP as SK Process
end
grpcClient->>grpcServer: UserRequestFeatureDocumentation <br/>gRPC
grpcServer->>SKP: StartDocumentGeneration <br/>SK event
SKP->>grpcServer: RequestUserReview (SK Topic)/<br/>RequestUserReviewDocumentationFromProcess (gRPC)
grpcServer->>grpcClient: RequestUserReviewDocumentation <br/>gRPC
grpcClient->>grpcServer: UserReviewedDocumentation <br/>gRPC
grpcServer->>SKP: UserApprovedDocument/UserRejectedDocument <br/>SK event
SKP->>grpcServer: PublishDocumentation (SK Topic)/<br/>PublishDocumentation (gRPC)
grpcServer->>grpcClient: ReceivePublishedDocumentation <br/>gRPC
```
1. When the `UserRequestFeatureDocumentation` gRPC request is received from the gRPC client, the server initiates an SK Process and emits the `StartDocumentGeneration` SK event.
2. The `RequestUserReview` topic is emitted when the `DocumentationApproved` event is triggered during the `ProofReadDocumentationStep`. This event invokes the `RequestUserReviewDocumentationFromProcess` gRPC method to communicate with the server.
3. The `RequestUserReviewDocumentationFromProcess` method updates the shared stream, which is used to communicate with the subscribers of `RequestUserReviewDocumentation`. The gRPC client then receives the document for review and approval.
4. The gRPC client can approve or reject the document using the `UserReviewedDocumentation` method to communicate with the server. The server then sends the `UserApprovedDocument` or `UserRejectedDocument` SK event to the SK Process.
5. The SK Process resumes, and the `PublishDocumentationStep` now has all the necessary parameters to execute. Upon execution, the `PublishDocumentation` topic is triggered, invoking the `PublishDocumentation` method on the gRPC server.
6. The PublishDocumentation method updates the shared stream used by `ReceivePublishedDocumentation`, ensuring that all subscribers receive the update of the latest published document
## Demo
### Requirements
- Have Dapr setup ready
- Build and Run the app
- Interact with the server by:
- Install and run `gRPCui` listening to the address `localhost:58641`:
```
./grpcui.exe -plaintext localhost:58641
```
or
- Use the `ProcessWithCloudEvents.Client` App and use it to interact with the server. This app uses gRPC Web, which interacts with the server through `localhost:58640`.
### Usage without UI
For interacting with the gRPC server, the
1. Build and run the app
2. Open 2 windows of `gRPCui` with the following methods:
- Window 1:
- Method name: `UserRequestFeatureDocumentation` and `UserReviewedDocumentation`
- Window 2:
- Method name: `RequestUserReviewDocumentation`
- Window 3:
- Method name: `ReceivePublishedDocumentation`
3. Select a process id to be used with all methods. Example: processId = "100"
4. Execute different methods in the following order:
1. `RequestUserReviewDocumentation` with Request Data:
```json
{
"processId": "100"
}
```
This will subscribe to any request for review done for the specific process id and a response will be received when the process emits a notification.
Set timeout to 30 seconds.
2. `UserRequestFeatureDocumentation` with Request Data:
```json
{
"title": "some product title",
"userDescription": "some user description",
"content": "some product content",
"processId": "100",
}
```
This request will kickstart the creation of a new process with the specific processId passing an initial event to the SK process.
5. Once the `RequestUserReviewDocumentation` is received, execute the following methods:
1. `ReceivePublishedDocumentation` with Request Data:
```json
{
"processId": "100"
}
```
This will subscribe to any request for review done for the specific process id and a response will be received when the process emits a notification.
Set timeout to 30 seconds.
2. `UserReviewedDocumentation` with Request Data:
```json
{
"documentationApproved": true,
"reason": "",
"processData":
{
"processId": "100"
}
}
```
### Debugging
For debugging and be able to set breakpoints in different stages of the app, you can:
- Install the [Visual Studio Dapr Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vs-dapr) and make use of it by making use of the `<root>\dotnet\dapr.yaml` file already in the repository.
or
- Set the `ProcessWithCloudEvents.Grpc` as startup app, run and attach the Visual Studio debugger:
```
dapr run --app-id processwithcloudevents-grpc --app-port 58640 --app-protocol http -- dotnet run --no-build
```
@@ -0,0 +1,194 @@
// 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;
/// <summary>
/// This gRPC service handles the generation of documents using/invoking a SK Process
/// </summary>
public class DocumentGenerationService : GrpcDocumentationGeneration.GrpcDocumentationGenerationBase
{
private readonly ILogger<DocumentGenerationService> _logger;
private readonly Kernel _kernel;
private readonly IActorProxyFactory _actorProxyFactory;
private readonly ConcurrentDictionary<string, ConcurrentBag<IServerStreamWriter<DocumentationContentRequest>>> _docReviewSubscribers;
private readonly ConcurrentDictionary<string, ConcurrentBag<IServerStreamWriter<DocumentationContentRequest>>> _publishDocumentSubscribers;
/// <summary>
/// Constructor for the <see cref="DocumentGenerationService"/>
/// </summary>
/// <param name="logger"></param>
/// <param name="kernel"></param>
/// <param name="actorProxy"></param>
public DocumentGenerationService(ILogger<DocumentGenerationService> logger, Kernel kernel, IActorProxyFactory actorProxy)
{
this._logger = logger;
this._kernel = kernel;
this._actorProxyFactory = actorProxy;
this._docReviewSubscribers = new();
this._publishDocumentSubscribers = new();
}
/// <summary>
/// Method that receives a request to generate documentation, this will start the SK process
/// defined in <see cref="DocumentGenerationProcess.CreateProcessBuilder"/> <br/>
/// It will use the processId passed in the request or generate a new one if not provided
/// </summary>
/// <param name="request"></param>
/// <param name="context"></param>
/// <returns></returns>
public override async Task<ProcessData> 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 };
}
/// <summary>
/// Method that receives a request to request user review of documentation, this will send a request to the client
/// if subscribed to the <see cref="RequestUserReviewDocumentation"/> method previously with the same process id.<br/>
/// This method is meant to be used within the SK process from the <see cref="DocumentGenerationGrpcClient"/> implementation.
/// </summary>
/// <param name="request"></param>
/// <param name="context"></param>
/// <returns></returns>
public override async Task<Empty> 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();
}
/// <summary>
/// Method that receives request to receive user review of documentation. <br/>
/// This is meant to be used by the external client
/// </summary>
/// <param name="request"></param>
/// <param name="responseStream"></param>
/// <param name="context"></param>
/// <returns></returns>
public override async Task RequestUserReviewDocumentation(ProcessData request, IServerStreamWriter<DocumentationContentRequest> 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.
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="request"></param>
/// <param name="context"></param>
/// <returns></returns>
public override async Task<Empty> 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();
}
/// <summary>
/// Method used to publish the generated documentation, this will send the documentation to the client
/// if subscribed to the <see cref="ReceivePublishedDocumentation"/> method with the same process id.<br/>
/// This method is meant to be used within the SK process from the <see cref="DocumentGenerationGrpcClient"/> implementation.
/// </summary>
/// <param name="request"></param>
/// <param name="context"></param>
/// <returns></returns>
public override async Task<Empty> 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();
}
/// <summary>
/// Method that receives request to receive published documentation from a specific process id.
/// This is meant to be used by the external client.
/// </summary>
/// <param name="request"></param>
/// <param name="responseStream"></param>
/// <param name="context"></param>
/// <returns></returns>
public override async Task ReceivePublishedDocumentation(ProcessData request, IServerStreamWriter<DocumentationContentRequest> 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.
}
}
}
@@ -0,0 +1,21 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"Http1": {
"Url": "http://*:58640",
"Protocols": "Http1"
},
"Http2": {
"Url": "http://*:58641",
"Protocols": "Http2"
}
}
}
}