Instrukcje: Human-in-the-Loop

Ostrzeżenie

Semantic Kernel Process Framework jest eksperymentalna, nadal w trakcie opracowywania i podlega zmianie.

Przegląd

W poprzednich sekcjach utworzyliśmy proces, który pomoże nam zautomatyzować tworzenie dokumentacji dla naszego nowego produktu. Nasz proces może teraz wygenerować dokumentację specyficzną dla naszego produktu i zapewnić, że spełnia nasze standardy jakości, przeprowadzając ją przez cykl korekty i edycji. W tej sekcji ulepszymy ten proces ponownie, wymagając od człowieka zatwierdzenia lub odrzucenia dokumentacji przed jej opublikowaniem. Elastyczność struktury procesów oznacza, że istnieje kilka sposobów, które możemy zrobić, ale w tym przykładzie przedstawimy integrację z zewnętrznym systemem pubsub do żądania zatwierdzenia.

Diagram przepływu dla naszego procesu ze wzorcem z człowiekiem w pętli.

Ustaw publikowanie na oczekiwanie na zatwierdzenie

Pierwszą zmianą, jaką musimy wprowadzić w procesie, jest to, aby etap publikowania czekał na zatwierdzenie, zanim opublikuje dokumentację. Jedną z opcji jest po prostu dodanie drugiego parametru zatwierdzenia do funkcji PublishDocumentation w PublishDocumentationStep. Działa to, ponieważ funkcja jądra w kroku zostanie wywołana tylko wtedy, gdy podano wszystkie wymagane parametry.

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

Wsparcie dla procesu Human-in-the-loop w języku Python będzie wkrótce dostępne.

W powyższym kodzie funkcja PublishDocumentation w PublishDocumentationStep będzie wywoływana tylko wtedy, gdy wygenerowana dokumentacja została wysłana do parametru document, a wynik zatwierdzenia został wysłany do parametru userApproval.

Możemy teraz ponownie użyć istniejącej logiki kroku ProofreadStep, aby dodatkowo wyemitować zdarzenie do naszego zewnętrznego systemu pubsub, który powiadomi osobę zatwierdzającą, że pojawiło się nowe żądanie.

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

Ponieważ chcemy opublikować nowo wygenerowaną dokumentację po zatwierdzeniu przez agenta sprawdzania, zatwierdzone dokumenty zostaną w kolejce w kroku publikowania. Ponadto użytkownik zostanie powiadomiony za pośrednictwem naszego zewnętrznego systemu Pub/Sub, który zawiera najnowsze informacje dotyczące dokumentu. Zaktualizujmy przepływ procesu, aby był zgodny z tym nowym projektem.

Wsparcie dla procesu Human-in-the-loop w języku Python będzie wkrótce dostępne.

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

Na koniec należy podać implementację interfejsu IExternalKernelProcessMessageChannel , ponieważ jest ona używana wewnętrznie przez nowy ProxyStepelement . Ten interfejs służy do emitowania komunikatów zewnętrznie. Implementacja tego interfejsu będzie zależeć od używanego systemu zewnętrznego. W tym przykładzie użyjemy niestandardowego klienta utworzonego do wysyłania komunikatów do zewnętrznego systemu pubsub.

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

Na koniec, aby umożliwić procesowi ProxyStep korzystanie z implementacji IExternalKernelProcessMessageChannel , w tym przypadku MyCloudEventClientmusimy prawidłowo go przekazać.

W przypadku korzystania z lokalnego środowiska uruchomieniowego, zaimplementowaną klasę można przekazać podczas wywoływania StartAsync na klasie KernelProcess.

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)

W przypadku korzystania ze środowiska uruchomieniowego Dapr należy wykonać instalację wodną poprzez wstrzyknięcie zależności podczas instalacji programu projektu.

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

Wsparcie dla procesu Human-in-the-loop w języku Python będzie wkrótce dostępne.

W przepływie procesu wprowadzono dwie zmiany:

  • Dodano zdarzenie wejściowe o nazwie HumanApprovalResponse, które zostanie przekierowane do parametru userApproval kroku docsPublishStep.
  • Ponieważ funkcja jądra w docsPublishStep ma teraz dwa parametry, musimy zaktualizować istniejącą trasę, aby określić nazwę parametru document.

Uruchom proces tak jak poprzednio i zwróć uwagę, że tym razem, gdy moduł sprawdzania zatwierdza wygenerowaną dokumentację i wysyła go do parametru document kroku docPublishStep, krok nie jest już wywoływany, ponieważ oczekuje na parametr userApproval. W tym momencie proces przechodzi w stan bezczynności, ponieważ nie ma żadnych kroków gotowych do wywołania, a wywołanie wykonane w celu rozpoczęcia procesu zwraca odpowiedź. Proces pozostanie w stanie bezczynności, dopóki nasz człowiek w pętli nie podejmie działania w celu zatwierdzenia lub odrzucenia żądania opublikowania. Gdy tak się stanie i wynik zostanie przekazany z powrotem do naszego programu, możemy ponownie uruchomić proces z wynikiem.

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

Wsparcie dla procesu Human-in-the-loop w języku Python będzie wkrótce dostępne.

Po ponownym uruchomieniu procesu z UserApprovedDocument, rozpocznie się od miejsca, w którym został przerwany, i wywoła docsPublishStep z userApproval ustawionym na true, a nasza dokumentacja zostanie opublikowana. Jeśli ponownie uruchomione zdarzenie UserRejectedDocument uruchomi funkcję ApplySuggestions na etapie docsGenerationStep, proces będzie kontynuowany tak jak poprzednio.

Proces został ukończony i pomyślnie dodaliśmy etap z udziałem człowieka do naszego procesu. Ten proces może teraz służyć do generowania dokumentacji dla naszego produktu, sprawdzania go i publikowania po jego zatwierdzeniu przez człowieka.