Handleiding: Human-in-the-Loop

Waarschuwing

De Semantic Kernel Process Framework is experimenteel, nog steeds in ontwikkeling en kan worden gewijzigd.

Overzicht

In de vorige secties hebben we een proces gemaakt om ons te helpen bij het automatiseren van het maken van documentatie voor ons nieuwe product. Ons proces kan nu documentatie genereren die specifiek is voor ons product en kan ervoor zorgen dat het aan onze kwaliteitsnorm voldoet door het te laten proeflezen en bewerken. In deze sectie zullen we dit proces opnieuw verbeteren door een persoon te verplichten de documentatie goed te keuren of af te wijzen voordat deze wordt gepubliceerd. De flexibiliteit van het procesframework betekent dat er verschillende manieren zijn waarop we dit kunnen doen, maar in dit voorbeeld laten we de integratie met een extern pubsubsysteem zien voor het aanvragen van goedkeuring.

Stroomdiagram voor ons proces met een human-in-the-loop-patroon.

Publiceren wachten op goedkeuring

De eerste wijziging die we moeten aanbrengen in het proces, is door de publicatiestap te laten wachten op de goedkeuring voordat de documentatie wordt gepubliceerd. Een optie is om gewoon een tweede parameter toe te voegen voor de goedkeuring aan de functie PublishDocumentation in de PublishDocumentationStep. Dit werkt omdat een KernelFunction in een stap alleen wordt aangeroepen wanneer alle vereiste parameters zijn opgegeven.

// A process step to publish documentation
public class PublishDocumentationStep : KernelProcessStep
{
    [KernelFunction]
    public DocumentInfo PublishDocumentation(DocumentInfo document, bool userApproval) // added the userApproval parameter
    {
        // Only publish the documentation if it has been approved
        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;
    }
}

Ondersteuning voor het gedrag van het Python Human-in-the-loop-proces is binnenkort beschikbaar.

Met de bovenstaande code wordt de PublishDocumentation functie in de PublishDocumentationStep alleen aangeroepen wanneer de gegenereerde documentatie is verzonden naar de parameter document en het resultaat van de goedkeuring is verzonden naar de parameter userApproval.

We kunnen nu de bestaande logica van ProofreadStep de stap opnieuw gebruiken om een gebeurtenis naar ons externe pubsubsysteem te verzenden, waarmee de menselijke fiatteur wordt geïnformeerd dat er een nieuwe aanvraag is.

// A process step to publish documentation
public class ProofReadDocumentationStep : KernelProcessStep
{
    ...

    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("DocumentationApproved", data: document, visibility: KernelProcessEventVisibility.Public);
    }
    ...
}

Omdat we de zojuist gegenereerde documentatie willen publiceren wanneer deze wordt goedgekeurd door de controleagent, worden de goedgekeurde documenten in de wachtrij geplaatst in de publicatiestap. Daarnaast ontvangt een mens een melding via ons externe pubsubsysteem met een update over het meest recente document. We gaan de processtroom bijwerken zodat deze overeenkomt met dit nieuwe ontwerp.

Ondersteuning voor het gedrag van het Python Human-in-the-loop-proces is binnenkort beschikbaar.

// Create the process builder
ProcessBuilder processBuilder = new("DocumentationGeneration");

// Add the steps
var infoGatheringStep = processBuilder.AddStepFromType<GatherProductInfoStep>();
var docsGenerationStep = processBuilder.AddStepFromType<GenerateDocumentationStepV2>();
var docsProofreadStep = processBuilder.AddStepFromType<ProofreadStep>();
var docsPublishStep = processBuilder.AddStepFromType<PublishDocumentationStep>();

// internal component that allows emitting SK events externally, a list of topic names
// is needed to link them to existing SK events
var proxyStep = processBuilder.AddProxyStep(["RequestUserReview", "PublishDocumentation"]);

// Orchestrate the events
processBuilder
    .OnInputEvent("StartDocumentGeneration")
    .SendEventTo(new(infoGatheringStep));

processBuilder
    .OnInputEvent("UserRejectedDocument")
    .SendEventTo(new(docsGenerationStep, functionName: "ApplySuggestions"));

// When external human approval event comes in, route it to the 'isApproved' parameter of the docsPublishStep
processBuilder
    .OnInputEvent("UserApprovedDocument")
    .SendEventTo(new(docsPublishStep, parameterName: "userApproval"));

// Hooking up the rest of the process steps
infoGatheringStep
    .OnFunctionResult()
    .SendEventTo(new(docsGenerationStep, functionName: "GenerateDocumentation"));

docsGenerationStep
    .OnEvent("DocumentationGenerated")
    .SendEventTo(new(docsProofreadStep));

docsProofreadStep
    .OnEvent("DocumentationRejected")
    .SendEventTo(new(docsGenerationStep, functionName: "ApplySuggestions"));

// When the proofreader approves the documentation, send it to the 'document' parameter of the docsPublishStep
// Additionally, the generated document is emitted externally for user approval using the pre-configured proxyStep
docsProofreadStep
    .OnEvent("DocumentationApproved")
    // [NEW] addition to emit messages externally
    .EmitExternalEvent(proxyStep, "RequestUserReview") // Hooking up existing "DocumentationApproved" to external topic "RequestUserReview"
    .SendEventTo(new(docsPublishStep, parameterName: "document"));

// When event is approved by user, it gets published externally too
docsPublishStep
    .OnFunctionResult()
    // [NEW] addition to emit messages externally
    .EmitExternalEvent(proxyStep, "PublishDocumentation");

var process = processBuilder.Build();
return process;

Ten slotte moet een implementatie van de interface IExternalKernelProcessMessageChannel worden verstrekt, omdat deze intern wordt gebruikt door de nieuwe ProxyStep. Deze interface wordt gebruikt om berichten extern te verzenden. De implementatie van deze interface is afhankelijk van het externe systeem dat u gebruikt. In dit voorbeeld gebruiken we een aangepaste client die we hebben gemaakt om berichten te verzenden naar een extern pubsubsysteem.

// Example of potential custom IExternalKernelProcessMessageChannel implementation 
public class MyCloudEventClient : IExternalKernelProcessMessageChannel
{
    private MyCustomClient? _customClient;

    // Example of an implementation for the process
    public async Task EmitExternalEventAsync(string externalTopicEvent, KernelProcessProxyMessage message)
    {
        // logic used for emitting messages externally.
        // Since all topics are received here potentially 
        // some if else/switch logic is needed to map correctly topics with external APIs/endpoints.
        if (this._customClient != null)
        {
            switch (externalTopicEvent) 
            {
                case "RequestUserReview":
                    var requestDocument = message.EventData.ToObject() as DocumentInfo;
                    // As an example only invoking a sample of a custom client with a different endpoint/api route
                    this._customClient.InvokeAsync("REQUEST_USER_REVIEW", requestDocument);
                    return;

                case "PublishDocumentation":
                    var publishedDocument = message.EventData.ToObject() as DocumentInfo;
                    // As an example only invoking a sample of a custom client with a different endpoint/api route
                    this._customClient.InvokeAsync("PUBLISH_DOC_EXTERNALLY", publishedDocument);
                    return;
            }
        }
    }

    public async ValueTask Initialize()
    {
        // logic needed to initialize proxy step, can be used to initialize custom client
        this._customClient = new MyCustomClient("http://localhost:8080");
        this._customClient.Initialize();
    }

    public async ValueTask Uninitialize()
    {
        // Cleanup to be executed when proxy step is uninitialized
        if (this._customClient != null)
        {
            await this._customClient.ShutdownAsync();
        }
    }
}

Ten slotte moeten we het proces ProxyStep in staat stellen om gebruik te maken van de IExternalKernelProcessMessageChannel implementatie, in dit geval MyCloudEventClient, door het correct te verbinden.

Wanneer u Lokale runtime gebruikt, kan de geïmplementeerde klasse worden doorgegeven bij het aanroepen van StartAsync de KernelProcess klasse.

KernelProcess process;
IExternalKernelProcessMessageChannel myExternalMessageChannel = new MyCloudEventClient();
// Start the process with the external message channel
await process.StartAsync(kernel, new KernelProcessEvent 
    {
        Id = inputEvent,
        Data = input,
    },
    myExternalMessageChannel)

Wanneer u Dapr Runtime gebruikt, moet het loodgieterwerk worden uitgevoerd via afhankelijkheidsinjectie bij het instellen van het project.

var builder = WebApplication.CreateBuilder(args);
...
// depending on the application a singleton or scoped service can be used
// Injecting SK Process custom client IExternalKernelProcessMessageChannel implementation
builder.Services.AddSingleton<IExternalKernelProcessMessageChannel, MyCloudEventClient>();

Ondersteuning voor het gedrag van het Python Human-in-the-loop-proces is binnenkort beschikbaar.

Er zijn twee wijzigingen aangebracht in de processtroom:

  • Er is een invoergebeurtenis met de naam HumanApprovalResponse toegevoegd die wordt doorgestuurd naar de parameter userApproval van de docsPublishStep stap.
  • Omdat de KernelFunction in docsPublishStep nu twee parameters heeft, moeten we de bestaande route bijwerken om de parameternaam van documentop te geven.

Voer het proces uit zoals u eerder hebt gedaan en u ziet dat deze keer dat de proofreader de gegenereerde documentatie goedkeurt en naar de parameter document van de docPublishStep stap verzendt, wordt de stap niet meer aangeroepen omdat deze wacht op de parameter userApproval. Op dit moment gaat het proces inactief omdat er geen stappen gereed zijn om te worden aangeroepen en de aanroep die we hebben gedaan om het proces te starten, wordt geretourneerd. Het proces blijft in deze niet-actieve status totdat onze 'human-in-the-loop' actie onderneemt om de publicatieaanvraag goed te keuren of af te wijzen. Zodra dit is gebeurd en het resultaat is teruggegeven aan ons programma, kunnen we het proces opnieuw starten met het resultaat.

// Restart the process with approval for publishing the documentation.
await process.StartAsync(kernel, new KernelProcessEvent { Id = "UserApprovedDocument", Data = true });

Ondersteuning voor het gedrag van het Python Human-in-the-loop-proces is binnenkort beschikbaar.

Wanneer het proces opnieuw wordt gestart met de UserApprovedDocument, hervat het proces waar het was gebleven en roept het docsPublishStep aan met userApproval ingesteld op true en wordt onze documentatie gepubliceerd. Als deze opnieuw wordt gestart met de UserRejectedDocument gebeurtenis, wordt ApplySuggestions de functie in de docsGenerationStep stap gestart en wordt het proces als voorheen voortgezet.

Het proces is nu voltooid en we hebben een human-in-the-loop-stap toegevoegd aan ons proces. Het proces kan nu worden gebruikt om documentatie te genereren voor ons product, het te controleren en te publiceren zodra het is goedgekeurd door een mens.