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,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="13.0.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>61efcc24-41eb-4a92-8ebe-64de14ed54dd</UserSecretsId>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Azure.CognitiveServices" />
<PackageReference Include="Aspire.Hosting.NodeJs" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.Dapr" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.NodeJS.Extensions" />
<PackageReference Include="MessagePack" /> <!-- Override vulnerable transitive dependency (2.5.192) from Aspire packages -->
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\ProcessFramework.Aspire.SignalR.ProcessOrchestrator\ProcessFramework.Aspire.SignalR.ProcessOrchestrator.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using CommunityToolkit.Aspire.Hosting.Dapr;
var builder = DistributedApplication.CreateBuilder(args);
var openai = builder.AddConnectionString("openAiConnectionName");
var processOrchestrator = builder.AddProject<Projects.ProcessFramework_Aspire_SignalR_ProcessOrchestrator>("processorchestrator")
.WithReference(openai)
.WithDaprSidecar(new DaprSidecarOptions
{
AppPort = 7207,
AppProtocol = "https"
});
// var frontend = builder.AddProject<Projects.ProcessFramework_Aspire_SignalR_Frontend>("frontend")
// .WithReference(processOrchestrator);
var frontend = builder.AddNpmApp("frontend", "../ProcessFramework.Aspire.SignalR.ReactFrontend", "dev")
.WithReference(processOrchestrator);
builder.Build().Run();
@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
},
"ConnectionStrings": {
"openAiConnectionName": "https://{account_name}.openai.azure.com/"
}
}
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Steps;
namespace ProcessFramework.SignalR;
/// <summary>
/// Components related to the SK Process for generating documentation
/// </summary>
public static class DocumentGenerationProcess
{
/// <summary>
/// SK Process events emitted by <see cref="DocumentGenerationProcess"/>
/// </summary>
public static class DocGenerationEvents
{
/// <summary>
/// Event to start the document generation process
/// </summary>
public const string StartDocumentGeneration = nameof(StartDocumentGeneration);
/// <summary>
/// Event emitted when the user rejects the document
/// </summary>
public const string UserRejectedDocument = nameof(UserRejectedDocument);
/// <summary>
/// Event emitted when the user approves the document
/// </summary>
public const string UserApprovedDocument = nameof(UserApprovedDocument);
}
/// <summary>
/// SK Process topics emitted by <see cref="DocumentGenerationProcess"/>
/// Topics are used to emit events to external systems
/// </summary>
public static class DocGenerationTopics
{
/// <summary>
/// Request user review document generation topic
/// </summary>
public const string RequestUserReview = nameof(RequestUserReview);
/// <summary>
/// Publish documentat generated topic
/// </summary>
public const string PublishDocumentation = nameof(PublishDocumentation);
}
/// <summary>
/// Creates a process builder for the Document Generation SK Process
/// </summary>
/// <param name="processName">name of the SK Process</param>
/// <returns>instance of <see cref="ProcessBuilder"/></returns>
public static ProcessBuilder CreateProcessBuilder(string processName = "DocumentationGeneration")
{
// Create the process builder
ProcessBuilder processBuilder = new(processName);
// Add the steps
var infoGatheringStep = processBuilder.AddStepFromType<GatherProductInfoStep>();
var docsGenerationStep = processBuilder.AddStepFromType<GenerateDocumentationStep>();
var docsProofreadStep = processBuilder.AddStepFromType<ProofReadDocumentationStep>();
var docsPublishStep = processBuilder.AddStepFromType<PublishDocumentationStep>();
var proxyStep = processBuilder.AddProxyStep("", [DocGenerationTopics.RequestUserReview, DocGenerationTopics.PublishDocumentation]);
// Orchestrate the external input events
processBuilder
.OnInputEvent(DocGenerationEvents.StartDocumentGeneration)
.SendEventTo(new(infoGatheringStep));
processBuilder
.OnInputEvent(DocGenerationEvents.UserRejectedDocument)
.SendEventTo(new(docsGenerationStep, functionName: GenerateDocumentationStep.ProcessFunctions.ApplySuggestions));
processBuilder
.OnInputEvent(DocGenerationEvents.UserApprovedDocument)
.SendEventTo(new(docsPublishStep, parameterName: "userApproval"));
// Hooking up the rest of the process steps
infoGatheringStep
.OnFunctionResult()
.SendEventTo(new ProcessFunctionTargetBuilder(docsGenerationStep, functionName: GenerateDocumentationStep.ProcessFunctions.GenerateDocs));
docsGenerationStep
.OnEvent(GenerateDocumentationStep.OutputEvents.DocumentationGenerated)
.SendEventTo(new ProcessFunctionTargetBuilder(docsProofreadStep));
docsProofreadStep
.OnEvent(ProofReadDocumentationStep.OutputEvents.DocumentationRejected)
.SendEventTo(new ProcessFunctionTargetBuilder(docsGenerationStep, functionName: GenerateDocumentationStep.ProcessFunctions.ApplySuggestions));
// When the proofreader approves the documentation, send it to the 'docs' parameter of the docsPublishStep
// Additionally, the generated document is emitted externally for user approval using the pre-configured proxyStep
docsProofreadStep
.OnEvent(ProofReadDocumentationStep.OutputEvents.DocumentationApproved)
.EmitExternalEvent(proxyStep, DocGenerationTopics.RequestUserReview)
.SendEventTo(new ProcessFunctionTargetBuilder(docsPublishStep, parameterName: "document"));
// When event is approved by user, it gets published externally too
docsPublishStep
.OnFunctionResult()
.EmitExternalEvent(proxyStep, DocGenerationTopics.PublishDocumentation);
return processBuilder;
}
}
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.SemanticKernel;
namespace ProcessFramework.SignalR;
public sealed class LocalEventProxyChannel : IExternalKernelProcessMessageChannel
{
private HubConnection? _hubConnection;
public Task EmitExternalEventAsync(string externalTopicEvent, KernelProcessProxyMessage message)
{
if (this._hubConnection == null)
{
throw new InvalidOperationException("Hub connection is not initialized.");
}
return this._hubConnection.InvokeAsync(externalTopicEvent, message);
}
public async ValueTask Initialize()
{
this._hubConnection = new HubConnectionBuilder()
.WithUrl(new Uri("https://localhost:7207/pfevents"))
.Build();
await this._hubConnection.StartAsync().ConfigureAwait(false);
}
public async ValueTask Uninitialize()
{
if (this._hubConnection == null)
{
throw new InvalidOperationException("Hub connection is not initialized.");
}
await this._hubConnection.StopAsync().ConfigureAwait(false);
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
namespace ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Models;
public class DocumentGenerationRequest
{
public string? ProcessId { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string UserDescription { get; set; } = string.Empty;
public bool DocumentationApproved { get; set; }
public string? Reason { get; set; }
public ProcessData? ProcessData { get; set; }
}
public class ProcessData
{
public string ProcessId { get; set; } = string.Empty;
}
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
namespace ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Models;
/// <summary>
/// Object used to store generated document data
/// Since this object is used as parameter and state type by multiple steps,
/// Its members must be public and serializable
/// </summary>
//[DataContract]
public class DocumentInfo
{
/// <summary>
/// Id of the document
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Title of the document
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Content of the document
/// </summary>
public string Content { get; set; } = string.Empty;
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
namespace ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Models;
/// <summary>
/// Object used in the <see cref="ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Steps.GatherProductInfoStep"/>
/// </summary>
public class ProductInfo
{
/// <summary>
/// Title of the product
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Content of the product
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// User comments
/// </summary>
public string UserInput { get; set; } = string.Empty;
}
@@ -0,0 +1,5 @@
GET https://localhost:7207/api/processdoc
Accept: application/json
###
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>
$(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0101,SKEXP0110,OPENAI001
</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapr.Actors" />
<PackageReference Include="Dapr.Actors.AspNetCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" />
<PackageReference Include="Aspire.Azure.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\..\..\..\..\src\Experimental\Process.Runtime.Dapr\Process.Runtime.Dapr.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Experimental\Process.Core\Process.Core.csproj" />
<ProjectReference
Include="..\..\..\..\..\src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj" />
<ProjectReference
Include="..\ProcessFramework.Aspire.SignalR.ServiceDefaults\ProcessFramework.Aspire.SignalR.ServiceDefaults.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,146 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.SemanticKernel;
using ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Models;
using ProcessFramework.SignalR;
var builder = WebApplication.CreateBuilder(args);
AppContext.SetSwitch("Microsoft.SemanticKernel.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true);
builder.AddServiceDefaults();
builder.AddAzureOpenAIClient("openAiConnectionName");
builder.Services.AddSingleton<IExternalKernelProcessMessageChannel, LocalEventProxyChannel>();
builder.Services.AddKernel().AddAzureOpenAIChatCompletion("gpt-4o");
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
options.MaximumReceiveMessageSize = 1024 * 1024 * 10; // 10 MB
});
// Configure Dapr
builder.Services.AddActors(static options =>
{
// Register the actors required to run Processes
options.AddProcessActors();
});
builder.Services.AddCors(options =>
{
options.AddPolicy(name: "AllowAll",
policy =>
{
policy.WithOrigins("http://localhost:5173") // Replace with your frontend's URL
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors("AllowAll");
// app.UseHttpsRedirection();
app.MapPost("/api/generate-doc", async (Kernel kernel, IExternalKernelProcessMessageChannel? externalMessageChannel, [FromBody] DocumentGenerationRequest request) =>
{
var processId = string.IsNullOrEmpty(request.ProcessId) ? Guid.NewGuid().ToString() : request.ProcessId;
var process = DocumentGenerationProcess.CreateProcessBuilder().Build();
var processEvent = 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 },
};
var processContext = await process.StartAsync(
processEvent,
processId);
return new ProcessData { ProcessId = processId };
})
.WithName("GenerateDocument");
app.MapPost("/api/reviewed-doc", async (Kernel kernel, IExternalKernelProcessMessageChannel? externalMessageChannel, [FromBody] DocumentGenerationRequest request) =>
{
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.ProcessId);
return Results.Ok("Process completed successfully");
})
.WithName("ReviewDocument");
app.MapDefaultEndpoints();
app.MapHub<MyHub>("/pfevents", options =>
{
options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
});
app.MapActorsHandlers();
app.Run();
public class MyHub : Hub
{
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await base.OnDisconnectedAsync(exception);
}
#pragma warning disable IDE1006
public async Task RequestUserReview(KernelProcessProxyMessage eventData)
{
var requestDocument = eventData.EventData!.ToObject() as DocumentInfo;
await Clients.All.SendAsync("RequestUserReview", new
{
Title = requestDocument!.Title,
AssistantMessage = "Document ready for user revision. Approve or reject document",
Content = requestDocument.Content,
ProcessData = new { ProcessId = eventData.ProcessId }
});
}
public async Task PublishDocumentation(KernelProcessProxyMessage eventData)
{
var publishedDocument = eventData.EventData!.ToObject() as DocumentInfo;
await Clients.All.SendAsync("PublishDocumentation", new
{
Title = publishedDocument!.Title,
AssistantMessage = "Published Document Ready",
Content = publishedDocument.Content,
ProcessData = new { ProcessId = eventData.ProcessId }
});
}
}
public static class ExternalEventTopics
{
public const string RequestMoreInfo = nameof(RequestMoreInfo);
public const string ReturnResult = nameof(ReturnResult);
}
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Models;
namespace ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Steps;
/// <summary>
/// Step that receives product information
/// </summary>
public class GatherProductInfoStep : KernelProcessStep
{
/// <summary>
/// Only step function that process the information passed
/// When there is only one function, there is no need to specify functionNames in the KernelFunction annotator
/// </summary>
/// <param name="productInfo">product information</param>
/// <returns></returns>
[KernelFunction]
public ProductInfo OnReceiveUserRequest(ProductInfo productInfo)
{
Console.WriteLine($"[{nameof(GatherProductInfoStep)}]:\tGathering product information for product named {productInfo.Title}");
// For example purposes we just return some fictional information.
productInfo.Content = """
Product Description:
GlowBrew is a revolutionary AI driven coffee machine with industry leading number of LEDs and programmable light shows. The machine is also capable of brewing coffee and has a built in grinder.
Product Features:
1. **Luminous Brew Technology**: Customize your morning ambiance with programmable LED lights that sync with your brewing process.
2. **AI Taste Assistant**: Learns your taste preferences over time and suggests new brew combinations to explore.
3. **Gourmet Aroma Diffusion**: Built-in aroma diffusers enhance your coffee's scent profile, energizing your senses before the first sip.
Troubleshooting:
- **Issue**: LED Lights Malfunctioning
- **Solution**: Reset the lighting settings via the app. Ensure the LED connections inside the GlowBrew are secure. Perform a factory reset if necessary.
""";
return productInfo;
}
}
@@ -0,0 +1,137 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Models;
namespace ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Steps;
/// <summary>
/// Step that generates document content
/// </summary>
public class GenerateDocumentationStep : KernelProcessStep<GenerateDocumentationState>
{
/// <summary>
/// Function names of the steps, to be refereced when hooking up the step in a SK process
/// </summary>
public static class ProcessFunctions
{
/// <summary>
/// Genereta Doc function name
/// </summary>
public const string GenerateDocs = nameof(GenerateDocs);
/// <summary>
/// Apply Suggestions function name
/// </summary>
public const string ApplySuggestions = nameof(ApplySuggestions);
}
/// <summary>
/// Output events of the step, using this since 2 steps emit the same output event
/// </summary>
public static class OutputEvents
{
/// <summary>
/// Document Generated output event
/// </summary>
public const string DocumentationGenerated = nameof(DocumentationGenerated);
}
internal GenerateDocumentationState _state = new();
private readonly string _systemPrompt =
"""
Your job is to write high quality and engaging customer facing documentation for a new product from Contoso. You will be provide with information
about the product in the form of internal documentation, specs, and troubleshooting guides and you must use this information and
nothing else to generate the documentation. If suggestions are provided on the documentation you create, take the suggestions into account and
rewrite the documentation. Make sure the product sounds amazing.
""";
/// <inheritdoc/>
public override ValueTask ActivateAsync(KernelProcessStepState<GenerateDocumentationState> state)
{
this._state = state.State!;
this._state.ChatHistory ??= new ChatHistory(this._systemPrompt);
return base.ActivateAsync(state);
}
/// <summary>
/// Function that generates documentation from the <see cref="ProductInfo"/> provided
/// </summary>
/// <param name="kernel">instance of <see cref="Kernel"/></param>
/// <param name="context">instance of <see cref="KernelProcessStepContext"/></param>
/// <param name="productInfo">content to be used for document generation</param>
/// <returns></returns>
[KernelFunction(ProcessFunctions.GenerateDocs)]
public async Task GenerateDocumentationAsync(Kernel kernel, KernelProcessStepContext context, ProductInfo productInfo)
{
Console.WriteLine($"[{nameof(GenerateDocumentationStep)}]:\tGenerating documentation for provided productInfo...");
// Add the new product info to the chat history
this._state.ChatHistory!.AddUserMessage($"Product Info:\n{productInfo.Title} - {productInfo.Content}");
// Get a response from the LLM
IChatCompletionService chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var generatedDocumentationResponse = await chatCompletionService.GetChatMessageContentAsync(this._state.ChatHistory!);
DocumentInfo generatedContent = new()
{
Id = Guid.NewGuid().ToString(),
Title = $"Generated document - {productInfo.Title}",
Content = generatedDocumentationResponse.Content!,
};
this._state!.LastGeneratedDocument = generatedContent;
await context.EmitEventAsync(OutputEvents.DocumentationGenerated, generatedContent);
}
/// <summary>
/// Function that integrates suggestion into document content
/// </summary>
/// <param name="kernel">instance of <see cref="Kernel"/></param>
/// <param name="context">instance of <see cref="KernelProcessStepContext"/></param>
/// <param name="suggestions">suggestions to be integrated into the document content</param>
/// <returns></returns>
[KernelFunction(ProcessFunctions.ApplySuggestions)]
public async Task ApplySuggestionsAsync(Kernel kernel, KernelProcessStepContext context, string suggestions)
{
Console.WriteLine($"[{nameof(GenerateDocumentationStep)}]:\tRewriting documentation with provided suggestions...");
// Add the new product info to the chat history
this._state.ChatHistory!.AddUserMessage($"Rewrite the documentation with the following suggestions:\n\n{suggestions}");
// Get a response from the LLM
IChatCompletionService chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var generatedDocumentationResponse = await chatCompletionService.GetChatMessageContentAsync(this._state.ChatHistory!);
DocumentInfo updatedContent = new()
{
Id = Guid.NewGuid().ToString(),
Title = $"Revised - {this._state?.LastGeneratedDocument.Title}",
Content = generatedDocumentationResponse.Content!,
};
this._state!.LastGeneratedDocument = updatedContent;
await context.EmitEventAsync(OutputEvents.DocumentationGenerated, updatedContent);
}
}
/// <summary>
/// State of <see cref="GenerateDocumentationStep"/>
/// State must be saved since data is shared across functions
/// </summary>
public sealed class GenerateDocumentationState
{
/// <summary>
/// Last Document generated data
/// </summary>
public DocumentInfo LastGeneratedDocument { get; set; } = new();
/// <summary>
/// Chat history
/// </summary>
public ChatHistory? ChatHistory { get; set; }
}
@@ -0,0 +1,97 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Models;
namespace ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Steps;
/// <summary>
/// Step that determines generated document readiness
/// </summary>
public class ProofReadDocumentationStep : KernelProcessStep
{
/// <summary>
/// SK Process Events emitted by <see cref="ProofReadDocumentationStep"/>
/// </summary>
public static class OutputEvents
{
/// <summary>
/// Document has errors and needs to be revised event
/// </summary>
public const string DocumentationRejected = nameof(DocumentationRejected);
/// <summary>
/// Document looks ok and can be processed by the next step
/// </summary>
public const string DocumentationApproved = nameof(DocumentationApproved);
}
private readonly string _systemPrompt = """"
Your job is to proofread customer facing documentation for a new product from Contoso. You will be provide with proposed documentation
for a product and you must do the following things:
1. Determine if the documentation is passes the following criteria:
1. Documentation must use a professional tone.
1. Documentation should be free of spelling or grammar mistakes.
1. Documentation should be free of any offensive or inappropriate language.
1. Documentation should be technically accurate.
2. If the documentation does not pass 1, you must write detailed feedback of the changes that are needed to improve the documentation.
"""";
/// <summary>
/// Determines whether the document is needs a revision or is ready to be processed by the next step
/// </summary>
/// <param name="kernel">instance of <see cref="Kernel"/></param>
/// <param name="context">instance of <see cref="KernelProcessStepContext"/></param>
/// <param name="document">document content that is verified</param>
/// <returns></returns>
[KernelFunction]
public async Task ProofreadDocumentationAsync(Kernel kernel, KernelProcessStepContext context, DocumentInfo document)
{
var chatHistory = new ChatHistory(this._systemPrompt);
chatHistory.AddUserMessage(document.Content);
// Use structured output to ensure the response format is easily parsable
var settings = new OpenAIPromptExecutionSettings()
{
ResponseFormat = typeof(ProofreadingResponse)
};
IChatCompletionService chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var proofreadResponse = await chatCompletionService.GetChatMessageContentAsync(chatHistory, executionSettings: settings);
var formattedResponse = JsonSerializer.Deserialize<ProofreadingResponse>(proofreadResponse.Content!);
Console.WriteLine($"[{nameof(ProofReadDocumentationStep)}]:\n\tGrade = {(formattedResponse!.MeetsExpectations ? "Pass" : "Fail")}\n\tExplanation = {formattedResponse.Explanation}\n\tSuggestions = {string.Join("\n\t\t", formattedResponse.Suggestions)}");
if (formattedResponse.MeetsExpectations)
{
// Events that are getting piped to steps that will be resumed, like PublishDocumentationStep.OnPublishDocumentation
// require events to be marked as public so they are persisted and restored correctly
await context.EmitEventAsync(OutputEvents.DocumentationApproved, data: document, visibility: KernelProcessEventVisibility.Public);
}
else
{
await context.EmitEventAsync(new()
{
Id = OutputEvents.DocumentationRejected,
// This event is getting piped to the GenerateDocumentationStep.ApplySuggestionsAsync step which expects a string with suggestions for the document
Data = $"Explanation = {formattedResponse.Explanation}, Suggestions = {string.Join(",", formattedResponse.Suggestions)} ",
});
}
}
private sealed class ProofreadingResponse
{
[Description("Specifies if the proposed documentation meets the expected standards for publishing.")]
public bool MeetsExpectations { get; set; }
[Description("An explanation of why the documentation does or does not meet expectations.")]
public string Explanation { get; set; } = "";
[Description("A lis of suggestions, may be empty if there no suggestions for improvement.")]
public List<string> Suggestions { get; set; } = [];
}
}
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Models;
namespace ProcessFramework.Aspire.SignalR.ProcessOrchestrator.Steps;
/// <summary>
/// Step that publishes the generated documentation
/// </summary>
public class PublishDocumentationStep : KernelProcessStep
{
/// <summary>
/// Function that publishes the generated documentation
/// </summary>
/// <param name="document">document to be published</param>
/// <param name="userApproval">approval from the user</param>
/// <returns><see cref="DocumentInfo"/></returns>
[KernelFunction]
public DocumentInfo OnPublishDocumentation(DocumentInfo document, bool userApproval)
{
if (userApproval)
{
// For example purposes we just write the generated docs to the console
Console.WriteLine($"[{nameof(PublishDocumentationStep)}]:\tPublishing product documentation approved by user: \n{document.Title}\n{document.Content}");
}
return document;
}
}
@@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http1"
}
}
}
@@ -0,0 +1,22 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,54 @@
# React App for SK Process with Cloud Events
## Getting Started
Follow the steps below to set up, run, and debug the React app for SK Process with Cloud Events.
### Prerequisites
- Node.js (LTS version recommended)
- Yarn (package manager)
### Installation
1. Navigate to the project directory:
```bash
cd <repo>/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client
```
2. Install the dependencies:
```bash
yarn install
```
Alternatively, you can use the existing Visual Studio Code task:
1. Open the Command Palette in Visual Studio Code (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS).
2. Search for and select `Tasks: Run Task`.
3. Choose the `yarn: install` task from the list to install the dependencies.
### Running the Application
1. Ensure that the backend server is running.
2. Start the development server:
```bash
yarn: run dev
```
Alternatively, you can use the existing Visual Studio Code task `yarn: run dev`.
3. Open your browser and navigate to `http://localhost:5173` to view the app.
### Usage
1. Select cloud technology to be used.
2. Select SK Process to be used.
3. Interact with the UI to send events/messages to the backend. The UI will display any incoming events/messages from the backend.
4. Use the provided buttons/inputs to trigger specific actions or events as needed.
### Debugging
1. Run the application.
2. Start the app in debug mode:
- If using Visual Studio Code, go to the "Run and Debug" panel and select `Launch Edge against localhost`.
3. Set breakpoints in your code to inspect and debug as needed.
For more details, refer to the official React documentation: [React Docs](https://reactjs.org/docs/getting-started.html).
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SK Processes + Cloud Events</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,39 @@
{
"name": "processwithcloudevents-client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fluentui-contrib/react-chat": "^0.2.2",
"@fluentui/react-components": "^9.72.7",
"@microsoft/signalr": "^8.0.7",
"@protobuf-ts/grpcweb-transport": "^2.9.6",
"@protobuf-ts/runtime": "^2.9.6",
"@protobuf-ts/runtime-rpc": "^2.9.6",
"axios": "^1.16.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-markdown": "^10.1.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@protobuf-ts/plugin": "^2.9.6",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^8.0.16"
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
@@ -0,0 +1,145 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
import { useState } from "react";
import "./App.css";
import { SignalRDocumentationGenerationClient } from "./services/signalr/documentGeneration.client";
import { ProcessFrameworkHttpClient } from "./services/signalr/ProcessFrameworkClient";
import {
Divider,
Title1,
makeStyles,
} from "@fluentui/react-components";
import { CloudTechnologiesDetails, CloudTechnology } from './common/AppConstants';
import GenerateDocsChat, {
DocumentReview,
NewDocument,
} from "./components/GenerateDocumentsChat";
interface AppProps {
signalRClient: SignalRDocumentationGenerationClient;
httpClient: ProcessFrameworkHttpClient;
}
const useStyles = makeStyles({
root: {
alignItems: "flex-start",
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
rowGap: "20px",
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
},
innerContainer: {
margin: "20px",
width: "96%",
height: "100%",
},
divider: {
marginTop: "20px",
marginBottom: "20px",
},
settingsContainer: {
display: "flex",
columnGap: "20px",
},
});
const App: React.FC<AppProps> = ({ signalRClient, httpClient }) => {
const styles = useStyles();
const [generatedDocuments, setGeneratedDocuments] = useState<NewDocument[]>(
[]
);
const [publishedDocuments, setPublishedDocuments] = useState<NewDocument[]>(
[]
);
const onCreateDocumentRequest = async (
document: NewDocument
): Promise<string> => {
try {
await httpClient.generateDocumentation({
processId: document.processId,
content: document.title ?? "",
title: document.title ?? "",
assistantMessage: "",
});
return document.processId;
} catch (error) {
console.error("[HTTP] Error creating document request", error);
return "";
}
};
const onUserReviewedDocument = async (
userReview: DocumentReview
): Promise<boolean> => {
try {
await httpClient.requestDocumentationReview({
processId: userReview.processId,
documentationApproved: userReview.accepted,
reason: userReview.suggestions,
});
return true;
} catch (error) {
console.error("[HTTP] Error submitting user review", error);
return false;
}
};
const subscribeToSpecificProcessId = async (processId: string): Promise<void> => {
signalRClient.subscribeToProcessEvents(processId, {
onPublishedDocument: (message) => {
setPublishedDocuments((prevDocs) => [
...prevDocs,
{
processId: message.processData?.processId ?? "",
content: message.content,
title: message.title,
},
]);
console.log("[SignalR] Published document received: ", message);
},
onDocumentForReview: (message) => {
setGeneratedDocuments((prevDocs) => [
...prevDocs,
{
processId: message.processData?.processId ?? "",
content: message.content,
title: message.title,
},
]);
console.log("[SignalR] Document for review received: ", message);
},
});
};
return (
<div className={styles.root}>
<div className={styles.innerContainer}>
<Title1>SK Processes with Cloud events</Title1>
<Divider className={styles.divider} />
<div className={styles.settingsContainer}></div>
<Divider className={styles.divider} />
<GenerateDocsChat
cloudTechnologyName={
CloudTechnologiesDetails.get(CloudTechnology.SIGNALR)!.name
}
onCreateNewDocument={onCreateDocumentRequest}
onUserReviewedDocument={onUserReviewedDocument}
subscribeToSpecificProcessId={subscribeToSpecificProcessId}
generatedDocuments={generatedDocuments}
publishedDocuments={publishedDocuments}
/>
</div>
</div>
);
};
export default App;
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
// When more process samples are added, add them to this enum and the AppPagesDetails map below.
// Additionally update the AppPagesDetails map to include the new process sample and its details.
export enum AppPages {
DocumentGeneration = "DocumentGeneration",
}
interface EnumDetails {
name: string;
description: string;
}
export const AppPagesDetails = new Map<AppPages, EnumDetails>([
[
AppPages.DocumentGeneration,
{
name: "Document Generation",
description:
"Demo used to show case document generation using different cloud technologies with SK Processes",
},
],
]);
// When more cloud technologies are added, add them to this enum and the CloudTechnologiesDetails map below.
// Additionally update the CloudTechnologiesDetails map to include the new technology and its details.
export enum CloudTechnology {
GRPC = "GRPC",
SIGNALR = "SIGNALR"
}
export const CloudTechnologiesDetails = new Map<CloudTechnology, EnumDetails>(
[
[CloudTechnology.GRPC, {name: "gRPC", description: "gRPC Protocol"}],
[CloudTechnology.SIGNALR, {name: "SignalR", description: "SignalR Protocol"}]
]
);
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
export enum ChatUser {
USER = "USER",
ASSISTANT = "ASSISTANT",
}
export interface ChatMessageContent {
sender: ChatUser;
content?: unknown;
action?: string;
timestamp?: string;
}
@@ -0,0 +1,356 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
import {
Button,
Card,
CardHeader,
Input,
Label,
makeStyles,
Popover,
PopoverSurface,
PopoverTrigger,
Spinner,
Title2,
useId,
} from "@fluentui/react-components";
import { useEffect, useState } from "react";
import Markdown from "react-markdown";
import { v4 as uuidv4 } from "uuid";
import { ChatMessageContent, ChatUser } from "../common/ChatConstants";
import { CheckIcon, RejectIcon } from "./Icons";
import SimpleChat from "./SimpleChat";
export interface NewDocument {
title?: string;
processId: string;
content?: string;
}
export interface DocumentReview {
accepted: boolean;
processId: string;
suggestions: string;
}
interface GenerateDocsChatProps {
cloudTechnologyName: string;
generatedDocuments: NewDocument[];
publishedDocuments: NewDocument[];
onCreateNewDocument: (document: NewDocument) => Promise<string>;
onUserReviewedDocument: (userReview: DocumentReview) => Promise<boolean>;
subscribeToSpecificProcessId: (processId: string) => Promise<void>;
}
const useStyles = makeStyles({
root: {
display: "flex",
flexDirection: "column",
rowGap: "8px",
width: "90%",
},
processIdContainer: {
display: "flex",
flexDirection: "column",
rowGap: "8px",
alignItems: "flex-end",
},
buttonsFamily: {
display: "flex",
columnGap: "40px",
},
suggestionsContainer: {
display: "flex",
flexDirection: "column",
rowGap: "8px",
},
newDocHeaderHeader: {
marginTop: "0",
},
headerContainer: {
display: "flex",
justifyContent: "space-between",
},
});
const GenerateDocsChat: React.FC<GenerateDocsChatProps> = ({
cloudTechnologyName,
generatedDocuments,
publishedDocuments,
onCreateNewDocument,
onUserReviewedDocument,
subscribeToSpecificProcessId,
}) => {
const styles = useStyles();
const newDocNameId = useId("input");
const docRejectedId = useId("input");
const [messages, setMessages] = useState<ChatMessageContent[]>([]);
const [processId, setProcessId] = useState<string>();
const [newDocContent, setNewDocContent] = useState<string>();
const [rejectSuggestions, setRejectSuggestions] = useState<string>();
const [creatingNewDocument, setCreatingNewDocument] =
useState<boolean>(false);
const [allowUserReview, setAllowUserReview] = useState<boolean>(false);
useEffect(() => {
if (processId) {
subscribeToSpecificProcessId(processId);
}
}, [processId]);
const formatNewDocumentString = (doc: NewDocument, header: string) => {
const content = `### Title: ${doc.title}\n### Content:\n${doc.content}`;
return (
<Card>
<CardHeader header={<Title2>{header}</Title2>} />
<Markdown>{content}</Markdown>
</Card>
);
};
useEffect(() => {
if (generatedDocuments.length > 0) {
const lastDoc = generatedDocuments[generatedDocuments.length - 1];
setMessages((prevMessages) => [
...prevMessages,
{
sender: ChatUser.ASSISTANT,
content: formatNewDocumentString(
lastDoc,
"Document for review"
),
timestamp: new Date().toLocaleString(),
},
]);
setAllowUserReview(true);
}
}, [generatedDocuments]);
useEffect(() => {
if (publishedDocuments.length > 0) {
const lastDoc = publishedDocuments[publishedDocuments.length - 1];
setMessages((prevMessages) => [
...prevMessages,
{
sender: ChatUser.ASSISTANT,
content: formatNewDocumentString(
lastDoc,
"Published document"
),
timestamp: new Date().toLocaleString(),
},
]);
setAllowUserReview(false);
}
}, [publishedDocuments]);
const onCreateNewDocumentClicked = () => {
if (newDocContent === "") {
alert("Document title cannot be empty");
return;
}
// Need to know processId to be able to subscribe to incoming events from this process once it is running
// processId is used as identifier to start/resume process
const newProcessId = uuidv4();
setProcessId(newProcessId);
setAllowUserReview(false);
onCreateNewDocument({
processId: newProcessId,
title: newDocContent ?? "",
})
.then((result) => {
setMessages((prevMessages) => [
...prevMessages,
{
sender: ChatUser.ASSISTANT,
action: `Document generated - ${result}`,
},
]);
})
.finally(() => {
setCreatingNewDocument(false);
});
setMessages((prevMessages) => [
...prevMessages,
{
sender: ChatUser.USER,
content: `User created new document request - ${newDocContent}`,
timestamp: new Date().toLocaleString(),
},
]);
setCreatingNewDocument(true);
};
const onClearChat = () => {
setMessages([]);
setProcessId("");
setRejectSuggestions("");
setAllowUserReview(false);
};
const onUserRejectedDocument = () => {
if (!processId) {
alert("Process id cannot be empty, create document first");
return;
}
if (!rejectSuggestions) {
alert(
"Must provide non empty suggestions on rejection of a document"
);
return;
}
onUserReviewedDocument({
accepted: false,
suggestions: rejectSuggestions ?? "",
processId: processId!,
}).then(() => {
setMessages((prevMessages) => [
...prevMessages,
{
sender: ChatUser.ASSISTANT,
action: "Document generated with suggestions",
},
]);
});
setMessages((prevMessages) => [
...prevMessages,
{
sender: ChatUser.USER,
content: `User rejected document providing suggestions: ${rejectSuggestions}`,
timestamp: new Date().toLocaleString(),
},
{ sender: ChatUser.ASSISTANT, action: "Document rejected" },
]);
setRejectSuggestions("");
};
const onUserApprovedDocument = () => {
if (!processId) {
alert("Process id cannot be empty, create document first");
return;
}
onUserReviewedDocument({
accepted: true,
suggestions: "",
processId: processId!,
}).then(() => {
setMessages((prevMessages) => [
...prevMessages,
{ sender: ChatUser.ASSISTANT, action: "Document Approved" },
]);
});
setMessages((prevMessages) => [
...prevMessages,
{
sender: ChatUser.USER,
content: "User send approval of document",
timestamp: new Date().toLocaleString(),
},
]);
setRejectSuggestions("");
};
return (
<div className={styles.root}>
<div className={styles.headerContainer}>
<Title2>Document Generation with {cloudTechnologyName}</Title2>
<div className={styles.processIdContainer}>
<Label>ProcessId : </Label>
<Label>{processId ?? "-"}</Label>
</div>
</div>
<SimpleChat messages={messages} />
<div className={styles.buttonsFamily}>
<Popover withArrow>
<PopoverTrigger>
<Button
appearance="primary"
icon={
creatingNewDocument ? (
<Spinner size="tiny" />
) : undefined
}
>
Create new document
</Button>
</PopoverTrigger>
<PopoverSurface>
<div>
<h3 className={styles.newDocHeaderHeader}>
New Document
</h3>
<Label htmlFor={newDocNameId}>Title/Name: </Label>
<Input
required
id={newDocNameId}
onChange={(_e, d) => setNewDocContent(d.value)}
/>
<Button onClick={onCreateNewDocumentClicked}>
Create
</Button>
</div>
</PopoverSurface>
</Popover>
<div>
<Button
icon={<CheckIcon />}
onClick={onUserApprovedDocument}
disabled={!allowUserReview}
>
Accept Document
</Button>
<Popover withArrow>
<PopoverTrigger>
<Button
icon={<RejectIcon />}
disabled={!allowUserReview}
>
Reject Document
</Button>
</PopoverTrigger>
<PopoverSurface>
<div className={styles.suggestionsContainer}>
<h3 className={styles.newDocHeaderHeader}>
Document Rejected - add suggestions
</h3>
<div>
<Label htmlFor={docRejectedId}>
Suggestions:{" "}
</Label>
<Input
onChange={(_e, d) =>
setRejectSuggestions(d.value)
}
required
id={docRejectedId}
/>
</div>
<Button onClick={onUserRejectedDocument}>
Send suggestions
</Button>
</div>
</PopoverSurface>
</Popover>
</div>
<Button appearance="subtle" onClick={onClearChat}>
Clear chat
</Button>
</div>
</div>
);
};
export default GenerateDocsChat;
@@ -0,0 +1,9 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
import { CheckmarkRegular, DismissRegular } from "@fluentui/react-icons";
export const CheckIcon = CheckmarkRegular;
export const RejectIcon = DismissRegular;
export const ExitIcon = DismissRegular;
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
import { Chat, ChatMessage, ChatMyMessage } from "@fluentui-contrib/react-chat";
import { Divider, makeStyles } from "@fluentui/react-components";
import { ChatMessageContent, ChatUser } from "../common/ChatConstants";
const useStyles = makeStyles({
chatContainer: {
minHeight: "50vh",
overflowY: "auto",
boxSizing: "border-box",
border: "1px solid #ccc",
maxHeight: "50vh",
width: "100%",
},
});
interface SimpleChatProps {
messages: ChatMessageContent[];
}
const SimpleChat: React.FC<SimpleChatProps> = ({ messages }) => {
const styles = useStyles();
const renderMessage = (message: ChatMessageContent) => {
if (message.action) {
return <Divider key={`action-${message.action}-${message.sender}`}>{message.action}</Divider>;
}
switch (message.sender) {
case ChatUser.ASSISTANT:
return <ChatMessage author={"Assistant"} timestamp={message.timestamp} key={`assistant-${message.timestamp}`}>{message.content}</ChatMessage>;
case ChatUser.USER:
return <ChatMyMessage author={"User"} timestamp={message.timestamp} key={`user-${message.timestamp}`}>{message.content}</ChatMyMessage>;
default:
return <></>;
}
};
return (
<Chat className={styles.chatContainer}>
{messages.map((m) => renderMessage(m))}
</Chat>
);
};
export default SimpleChat;
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
import { createRoot } from "react-dom/client";
import { SignalRDocumentationGenerationClient } from './services/signalr/documentGeneration.client';
import { ProcessFrameworkHttpClient } from './services/signalr/ProcessFrameworkClient';
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import "./index.css";
import App from "./App.tsx";
const signalRClient = new SignalRDocumentationGenerationClient();
const httpClient = new ProcessFrameworkHttpClient();
createRoot(document.getElementById("root")!).render(
<FluentProvider theme={webLightTheme}>
<App signalRClient={signalRClient} httpClient={httpClient} />
</FluentProvider>
);
@@ -0,0 +1,32 @@
import axios from 'axios';
const API_BASE_URL = 'http://localhost:5125'; // Replace with actual backend endpoint
export class ProcessFrameworkHttpClient {
async generateDocumentation(request: {
processId: string;
content: string;
title: string;
assistantMessage: string;
}): Promise<void> {
try {
await axios.post(`${API_BASE_URL}/api/generate-doc`, request);
} catch (error) {
console.error('[HTTP] Error publishing documentation', error);
throw error;
}
}
async requestDocumentationReview(request: {
processId: string;
documentationApproved: boolean;
reason: string;
}): Promise<void> {
try {
await axios.post(`${API_BASE_URL}/api/reviewed-doc`, request);
} catch (error) {
console.error('[HTTP] Error requesting documentation review', error);
throw error;
}
}
}
@@ -0,0 +1,56 @@
import * as signalR from "@microsoft/signalr";
import {
DocumentationApprovalRequest,
DocumentationContentRequest,
FeatureDocumentationRequest,
} from "../signalr/documentGeneration";
export class SignalRDocumentationGenerationClient {
private connection: signalR.HubConnection;
constructor() {
this.connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5125/pfevents") // Replace with your SignalR hub URL
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
this.connection.on("RequestUserReview", (message: any) => {
console.log("Received message from SignalR:", message);
// Handle the received message here
});
this.connection.on("PublishDocumentation", (message: any) => {
console.log("Received message from SignalR:", message);
// Handle the received message here
});
this.connection.start()
.then(() => console.log("SignalR connection established"))
.catch((error) => console.error("Could not establish SignalR connection", error));
}
async userRequestFeatureDocumentation(input: FeatureDocumentationRequest): Promise<any> {
return this.connection.invoke("UserRequestFeatureDocumentation", input);
}
async requestUserReviewDocumentationFromProcess(input: DocumentationContentRequest): Promise<void> {
return this.connection.invoke("RequestUserReviewDocumentationFromProcess", input);
}
async userReviewedDocumentation(input: DocumentationApprovalRequest): Promise<void> {
return this.connection.invoke("UserReviewedDocumentation", input);
}
async publishDocumentation(input: DocumentationContentRequest): Promise<void> {
return this.connection.invoke("PublishDocumentation", input);
}
subscribeToProcessEvents(_processId: string, handlers: {
onPublishedDocument: (message: any) => void;
onDocumentForReview: (message: any) => void;
}): void {
this.connection.on("PublishDocumentation", handlers.onPublishedDocument);
this.connection.on("RequestUserReview", handlers.onDocumentForReview);
}
}
@@ -0,0 +1,23 @@
export interface FeatureDocumentationRequest {
title: string;
userDescription: string;
content: string;
processId: string;
}
export interface DocumentationContentRequest {
title: string;
content: string;
assistantMessage: string;
processData?: ProcessData;
}
export interface DocumentationApprovalRequest {
documentationApproved: boolean;
reason: string;
processData?: ProcessData;
}
export interface ProcessData {
processId: string;
}
@@ -0,0 +1,5 @@
/*
* Copyright (c) 2025 Microsoft
* All rights reserved.
*/
/// <reference types="vite/client" />
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"noImplicitAny": false
},
"include": ["src"]
}
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
@@ -0,0 +1,169 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
/// <summary>
/// Provides extension methods for adding common .NET Aspire services, including service discovery,
/// resilience, health checks, and OpenTelemetry.
/// </summary>
public static class CommonExtensions
{
private const string HealthEndpointPath = "/health";
private const string AlivenessEndpointPath = "/alive";
/// <summary>s
/// Adds default services to the application, including OpenTelemetry, health checks,
/// service discovery, and HTTP client defaults with resilience and service discovery enabled.
/// </summary>
/// <typeparam name="TBuilder">The type of the host application builder.</typeparam>
/// <param name="builder">The host application builder instance.</param>
/// <returns>The updated host application builder.</returns>
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();
// Turn on service discovery by default
http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });
return builder;
}
/// <summary>
/// Configures OpenTelemetry for the application, including logging, metrics, and tracing.
/// </summary>
/// <typeparam name="TBuilder">The type of the host application builder.</typeparam>
/// <param name="builder">The host application builder instance.</param>
/// <returns>The updated host application builder.</returns>
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
if (builder.Configuration["ConnectionStrings:openAiConnectionName"] is not null)
{
builder.Logging.AddTraceSource("Microsoft.SemanticKernel");
}
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
if (builder.Configuration["ConnectionStrings:openAiConnectionName"] is not null)
{
metrics.AddMeter("Microsoft.SemanticKernel*");
}
})
.WithTracing(tracing =>
{
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation(tracing =>
// Exclude health check requests from tracing
tracing.Filter = context =>
!context.Request.Path.StartsWithSegments(HealthEndpointPath)
&& !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
)
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
//.AddGrpcClientInstrumentation()
.AddHttpClientInstrumentation();
if (builder.Configuration["ConnectionStrings:openAiConnectionName"] is not null)
{
tracing.AddSource("Microsoft.SemanticKernel*");
}
});
builder.AddOpenTelemetryExporters();
return builder;
}
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
//{
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}
return builder;
}
/// <summary>
/// Adds default health checks to the application, including a liveness check to ensure the app is responsive.
/// </summary>
/// <typeparam name="TBuilder">The type of the host application builder.</typeparam>
/// <param name="builder">The host application builder instance.</param>
/// <returns>The updated host application builder.</returns>
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
/// <summary>
/// Maps default health check endpoints for the application.
/// Adds "/health" and "/alive" endpoints in development environments.
/// </summary>
/// <param name="app">The web application instance.</param>
/// <returns>The updated web application instance.</returns>
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment())
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks(HealthEndpointPath);
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>
</Project>