Compartir a través de


Guía: Humano en el Bucle

Advertencia

El marco de proceso de kernel semántico es experimental, aún en desarrollo y está sujeto a cambios.

Visión general

En las secciones anteriores creamos un proceso para ayudarnos a automatizar la creación de documentación para nuestro nuevo producto. Nuestro proceso ahora puede generar documentación específica de nuestro producto y puede asegurarse de que cumple nuestra barra de calidad ejecutándola a través de un ciclo de revisión y edición. En esta sección, mejoraremos de nuevo ese proceso exigiendo a un usuario que apruebe o rechace la documentación antes de que se publique. La flexibilidad del marco de proceso significa que hay varias maneras de hacerlo, pero en este ejemplo demostraremos la integración con un sistema pubsub externo para solicitar la aprobación.

diagrama de flujo para nuestro proceso con un patrón de intervención humana.

Hacer que la publicación espere la aprobación

El primer cambio que necesitamos realizar en el proceso es hacer que el paso de publicación espere a la aprobación antes de publicar la documentación. Una opción consiste simplemente en agregar un segundo parámetro para la aprobación a la función PublishDocumentation en el PublishDocumentationStep. Esto funciona porque un kernelFunction en un paso solo se invocará cuando se hayan proporcionado todos sus parámetros necesarios.

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

Soporte para el comportamiento del proceso de Python con intervención humana estará disponible pronto.

Con el código anterior, la función PublishDocumentation del PublishDocumentationStep solo se invocará cuando se haya enviado la documentación generada al parámetro document y el resultado de la aprobación se haya enviado al parámetro userApproval.

Ahora podemos reutilizar la lógica existente del paso ProofreadStep para emitir adicionalmente un evento a nuestro sistema externo de pubsub, que notificará al aprobador humano de que hay una nueva solicitud.

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

Dado que queremos publicar la documentación recién generada cuando sea aprobada por el agente de revisión, los documentos aprobados se pondrán en cola en el paso de publicación. Además, se notificará a una persona a través de nuestro sistema externo de publicación-suscripción con una actualización sobre el documento más reciente. Vamos a actualizar el flujo de proceso para que coincida con este nuevo diseño.

Soporte para el comportamiento del proceso de Python con intervención humana estará disponible pronto.

// 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, se debe proporcionar una implementación de la interfaz IExternalKernelProcessMessageChannel ya que es utilizada internamente por el nuevo ProxyStep. Esta interfaz se usa para emitir mensajes externamente. La implementación de esta interfaz dependerá del sistema externo que estés utilizando. En este ejemplo, usaremos un cliente personalizado que hemos creado para enviar mensajes a un sistema de publicación/suscripción 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 el proceso ProxyStep haga uso de la implementación IExternalKernelProcessMessageChannel, en este caso MyCloudEventClient, necesitamos ajustarlo adecuadamente.

Al utilizar el entorno de ejecución local, se puede pasar la clase implementada al invocar StartAsync en la clase 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)

Al utilizar Dapr Runtime, la integración debe hacerse a través de la inyección de dependencias en la configuración del Programa del proyecto.

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

Soporte para el comportamiento del proceso de Python con intervención humana estará disponible pronto.

Se han realizado dos cambios en el flujo de proceso:

  • Se agregó un evento de entrada denominado HumanApprovalResponse que se enrutará al parámetro userApproval del paso de docsPublishStep.
  • Puesto que KernelFunction de docsPublishStep ahora tiene dos parámetros, es necesario actualizar la ruta existente para especificar el nombre del parámetro de document.

Ejecute el proceso como hizo antes y observe que esta vez cuando el corrector aprueba la documentación generada y la envía al parámetro document del paso docPublishStep, el paso ya no se invoca porque está esperando el parámetro userApproval. En este momento, el proceso se vuelve inactivo porque no hay pasos listos para invocarse y la llamada que realizamos para iniciar el proceso retorna. El proceso permanecerá en este estado inactivo hasta que la persona involucrada actúe para aprobar o rechazar la solicitud de publicación. Una vez que esto ha ocurrido y el resultado se ha comunicado de nuevo a nuestro programa, podemos reiniciar el proceso con el resultado.

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

Soporte para el comportamiento del proceso de Python con intervención humana estará disponible pronto.

Cuando el proceso se inicie de nuevo con el UserApprovedDocument, continuará desde donde se lo dejó y luego invocará al docsPublishStep con userApproval configurado en true, y la documentación se publicará. Si se inicia nuevamente con el evento UserRejectedDocument, el proceso activará la función ApplySuggestions en el paso docsGenerationStep y el proceso continuará como antes.

El proceso ahora está completo y hemos añadido con éxito un paso de intervención humana a nuestro proceso. Ahora se puede utilizar el proceso para generar documentación para nuestro producto, corregirlo y publicarlo una vez que haya sido aprobado por una persona.