Compartilhar via


Instruções: Human-in-the-Loop

Aviso

A Estrutura de Processo de Kernel Semântico é experimental, ainda em desenvolvimento e está sujeita a alterações.

Visão geral

Nas seções anteriores, criamos um Processo para nos ajudar a automatizar a criação de documentação para nosso novo produto. Nosso processo agora pode gerar documentação específica para o nosso produto e garantir que ela atenda ao nosso padrão de qualidade, passando-a por um ciclo de revisão e edição. Nesta seção, melhoraremos esse processo novamente exigindo que um humano aprove ou rejeite a documentação antes de ser publicada. A flexibilidade da estrutura de processo significa que há várias maneiras de fazer isso, mas neste exemplo demonstraremos a integração com um sistema pubsub externo para solicitar aprovação.

Diagrama de fluxo para nosso processo com um padrão humano no loop.

Fazer com que a publicação aguarde a aprovação

A primeira alteração que precisamos fazer no processo é fazer com que a etapa de publicação aguarde a aprovação antes de publicar a documentação. Uma opção é simplesmente adicionar um segundo parâmetro para a aprovação à função PublishDocumentation no PublishDocumentationStep. Isso funciona porque um KernelFunction em uma etapa só será invocado quando todos os parâmetros necessários forem fornecidos.

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

O suporte para o comportamento do Processo humano no loop do Python está chegando em breve.

Com o código acima, a função PublishDocumentation no PublishDocumentationStep será invocada somente quando a documentação gerada tiver sido enviada para o parâmetro document e o resultado da aprovação tiver sido enviado para o parâmetro userApproval.

Agora podemos reutilizar a lógica existente da ProofreadStep etapa para emitir adicionalmente um evento para nosso sistema pubsub externo que notificará o aprovador humano de que há uma nova solicitação.

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

Como queremos publicar a documentação recém-gerada quando ela for aprovada pelo agente revisão de texto, os documentos aprovados serão enfileirados na etapa de publicação. Além disso, uma pessoa será notificada através de nosso sistema pubsub externo com uma atualização sobre o documento mais recente. Vamos atualizar o fluxo do processo para corresponder a esse novo design.

O suporte para o comportamento do Processo humano no loop do Python está chegando em breve.

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

Por fim, uma implementação da interface IExternalKernelProcessMessageChannel deve ser fornecida, pois ela é usada internamente pelo novo ProxyStep. Essa interface é usada para emitir mensagens externamente. A implementação dessa interface dependerá do sistema externo que você está usando. Neste exemplo, usaremos um cliente personalizado que criamos para enviar mensagens para um sistema pubsub externo.

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

Por fim, para permitir que o processo ProxyStep faça uso da implementação IExternalKernelProcessMessageChannel, nesse caso MyCloudEventClient, precisamos encaminhá-la corretamente.

Ao usar o Runtime Local, a classe implementada pode ser passada ao invocar StartAsync a KernelProcess classe.

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)

Ao usar o Dapr Runtime, o encanamento deve ser feito por meio da injeção de dependência na instalação do programa do projeto.

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

O suporte para o comportamento do Processo humano no loop do Python está chegando em breve.

Duas alterações foram feitas no fluxo do processo:

  • Foi adicionado um evento de entrada chamado HumanApprovalResponse que será roteado para o parâmetro userApproval da etapa docsPublishStep.
  • Como o KernelFunction no docsPublishStep agora tem dois parâmetros, precisamos atualizar a rota existente para especificar o nome do parâmetro de document.

Execute o processo como você fez antes e observe que, desta vez, quando o revisor aprova a documentação gerada e a envia para o parâmetro document da etapa docPublishStep, a etapa não é mais invocada porque está aguardando o parâmetro userApproval. Neste ponto, o processo fica ocioso porque não há etapas prontas para serem invocadas e a chamada que fizemos para iniciar o processo retorna. O processo permanecerá nesse estado ocioso até que nosso "human-in-the-loop" tome uma ação para aprovar ou rejeitar o pedido de publicação. Depois que isso acontecer e o resultado tiver sido comunicado de volta ao nosso programa, poderemos reiniciar o processo com o resultado.

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

O suporte para o comportamento do Processo humano no loop do Python está chegando em breve.

Quando o processo for iniciado novamente com o UserApprovedDocument ele retomará de onde parou e invocará o docsPublishStep com userApproval definido como true e nossa documentação será publicada. Se ele for iniciado novamente com o UserRejectedDocument evento, o processo iniciará a ApplySuggestions função na docsGenerationStep etapa e o processo continuará como antes.

O processo agora está concluído e adicionamos com êxito uma etapa de intervenção humana ao nosso processo. O processo agora pode ser usado para gerar documentação para nosso produto, revisá-lo e publicá-lo depois que ele tiver sido aprovado por um humano.