Freigeben über


Anleitung: Human-in-the-Loop

Warnung

Das semantische Kernel-Prozess-Framework ist Experimentell, befindet sich noch in der Entwicklung und unterliegt Änderungen.

Überblick

In den vorherigen Abschnitten haben wir einen Prozess erstellt, der uns dabei hilft, die Erstellung von Dokumentationen für unser neues Produkt zu automatisieren. Unser Prozess kann jetzt Dokumentationen generieren, die für unser Produkt spezifisch sind, und sicherstellen, dass sie unsere Qualitätsleiste erfüllt, indem sie über einen Korrekturlesen- und Bearbeitungszyklus ausgeführt wird. In diesem Abschnitt werden wir diesen Prozess erneut verbessern, indem ein Mensch die Dokumentation genehmigen oder ablehnen muss, bevor sie veröffentlicht wird. Die Flexibilität des Prozessframeworks bedeutet, dass es mehrere Möglichkeiten gibt, wie wir dies tun könnten, aber in diesem Beispiel zeigen wir die Integration mit einem externen Pubsub-System für die Anforderung der Genehmigung.

Flussdiagramm für unseren Prozess mit einem Muster, bei dem der Mensch im Mittelpunkt steht.

Veröffentlichung zur Genehmigung verzögern

Die erste Änderung, die wir am Prozess vornehmen müssen, besteht darin, den Veröffentlichungsschritt auf die Genehmigung zu warten, bevor sie die Dokumentation veröffentlicht. Eine Möglichkeit besteht darin, einfach einen zweiten Parameter für die Genehmigung der PublishDocumentation-Funktion im PublishDocumentationStephinzuzufügen. Dies funktioniert, da eine KernelFunction in einem Schritt nur aufgerufen wird, wenn alle erforderlichen Parameter bereitgestellt wurden.

// 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;
    }
}

Unterstützung für das Python-Mensch-in-der-Schleife-Prozessverhalten kommt bald.

Mit dem obigen Code wird die PublishDocumentation-Funktion im PublishDocumentationStep nur aufgerufen, wenn die generierte Dokumentation an den parameter document gesendet wurde und das Ergebnis der Genehmigung an den userApproval Parameter gesendet wurde.

Wir können nun die bestehende Logik des ProofreadStep Schritts wiederverwenden, um zusätzlich ein Ereignis an unser externes PubSub-System zu senden, das den menschlichen Prüfer darüber informiert, dass eine neue Anfrage vorliegt.

// 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);
    }
    ...
}

Da wir die neu erstellte Dokumentation veröffentlichen möchten, sobald sie vom Korrekturagenten genehmigt worden ist, werden die genehmigten Dokumente im Veröffentlichungsprozess eingereiht. Darüber hinaus wird eine Person über unser externes Pub-Sub-System über eine Aktualisierung des neuesten Dokuments informiert. Lassen Sie uns den Prozessfluss so aktualisieren, dass er diesem neuen Design entspricht.

Unterstützung für das Python-Mensch-in-der-Schleife-Prozessverhalten kommt bald.

// 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;

Schließlich sollte eine Implementierung der Schnittstelle IExternalKernelProcessMessageChannel bereitgestellt werden, da sie intern vom neuen ProxyStep verwendet wird. Diese Schnittstelle wird verwendet, um Nachrichten extern zu senden. Die Implementierung dieser Schnittstelle hängt von dem externen System ab, das Sie verwenden. In diesem Beispiel verwenden wir einen benutzerdefinierten Client, den wir erstellt haben, um Nachrichten an ein externes Pubsub-System zu senden.

// 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();
        }
    }
}

Schließlich, um zu ermöglichen, dass der Prozess ProxyStep von der Implementierung IExternalKernelProcessMessageChannel Gebrauch macht, in diesem Fall MyCloudEventClient, müssen wir ihn richtig weiterleiten.

Bei Verwendung von Local Runtime kann die implementierte Klasse an die KernelProcess-Klasse übergeben werden, indem StartAsync aufgerufen wird.

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)

Bei der Verwendung von Dapr Runtime muss die interne Verkabelung über Dependency Injection während der Programmeinrichtung des Projekts erfolgen.

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>();

Unterstützung für das Python-Mensch-in-der-Schleife-Prozessverhalten kommt bald.

Am Prozessablauf wurden zwei Änderungen vorgenommen:

  • Es wurde ein Eingabeereignis namens HumanApprovalResponse hinzugefügt, das an den userApproval Parameter des docsPublishStep Schritts weitergeleitet wird.
  • Da die KernelFunction in docsPublishStep jetzt über zwei Parameter verfügt, müssen wir die vorhandene Route aktualisieren, um den Parameternamen documentanzugeben.

Führen Sie den Prozess wie zuvor aus, und beachten Sie, dass diesmal, wenn der Korrekturleser die generierte Dokumentation genehmigt und an den document-Parameter des docPublishStep-Schritts sendet, der Schritt nicht mehr aufgerufen wird, da er auf den userApproval-Parameter wartet. An diesem Punkt ruht der Prozess, da es keine Schritte gibt, die aufgerufen werden können, und der von uns zum Starten des Prozesses getätigte Aufruf zurückgegeben wird. Der Prozess verbleibt in diesem Leerlaufzustand, bis unser sogenannter "Human-in-the-Loop" tätig wird, um die Veröffentlichungsanforderung zu genehmigen oder abzulehnen. Sobald dies geschehen ist und das Ergebnis wieder an unser Programm kommuniziert wurde, können wir den Prozess mit dem Ergebnis neu starten.

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

Unterstützung für das Python-Mensch-in-der-Schleife-Prozessverhalten kommt bald.

Wenn der Prozess erneut mit dem UserApprovedDocument gestartet wird, setzt er dort fort, wo er aufgehört hatte, ruft dann die docsPublishStep mit userApproval, das auf true festgelegt ist, auf, und unsere Dokumentation wird veröffentlicht. Wenn der Vorgang mit dem UserRejectedDocument-Ereignis erneut gestartet wird, wird der Prozess die ApplySuggestions-Funktion im docsGenerationStep-Schritt ausführen und der Prozess wird wie zuvor fortgesetzt.

Der Prozess ist nun abgeschlossen und wir haben erfolgreich einen „human-in-the-loop“-Schritt in unseren Prozess integriert. Der Prozess kann nun verwendet werden, um Dokumentationen für unser Produkt zu generieren, es zu korrekturlesen und zu veröffentlichen, nachdem es von einem Menschen genehmigt wurde.