Partilhar via


Como fazer: Human-in-the-Loop

Advertência

O Semantic Kernel Process Framework é experimental, ainda está em desenvolvimento e está sujeito a alterações.

Visão geral

Nas seções anteriores, construímos 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 pode garantir que ele atenda aos nossos padrões de qualidade, passando-o 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 que ela seja publicada. A flexibilidade da estrutura do 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.

Fluxograma para o nosso processo com um padrão de intervenção humana.

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

A primeira mudança 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 uma KernelFunction em uma etapa só será invocada quando todos os seus parâmetros necessários tiverem sido 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 Python Human-in-the-loop Process estará disponível em breve.

Com o código acima, a função PublishDocumentation no PublishDocumentationStep só será invocada 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 de ProofreadStep passo existente para emitir adicionalmente um evento para o 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 revisor, os documentos aprovados serão enfileirados na etapa de publicação. Além disso, uma pessoa será notificada através do nosso sistema de publicação e subscrição 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 Python Human-in-the-loop Process estará disponível 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;

Finalmente, uma implementação da interface IExternalKernelProcessMessageChannel deve ser fornecida, uma vez que é usado internamente pelo novo ProxyStep. Esta interface é usada para emitir mensagens externamente. A implementação desta interface dependerá do sistema externo que estiver a utilizar. 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();
        }
    }
}

Finalmente, para permitir que o processo ProxyStep faça uso da IExternalKernelProcessMessageChannel implementação, neste caso MyCloudEventClient, precisamos canalizá-lo corretamente.

Ao usar o Local Runtime, 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 de injeção de dependência na configuraçã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 Python Human-in-the-loop Process estará disponível em breve.

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

  • Adicionado um evento de entrada chamado HumanApprovalResponse que será roteado para o parâmetro userApproval da etapa docsPublishStep.
  • Como o KernelFunction em 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 inativo até que o nosso "humano no circuito" intervenha para aprovar ou rejeitar o pedido de publicação. Uma vez que isso tenha acontecido e o resultado tenha sido comunicado de volta ao nosso programa, podemos 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 Python Human-in-the-loop Process estará disponível em breve.

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

O processo está agora concluído e adicionámos uma etapa human-in-the-loop ao nosso processo. O processo agora pode ser usado para gerar documentação para o nosso produto, revisá-lo e publicá-lo depois de ter sido aprovado por um ser humano.