사람 개입(Human-in-the-Loop) 다루기
How-To: Human-in-the-Loop
[!WARNING] _Semantic Kernel Process Framework_는 실험 단계이고, 아직 개발 중이며 언제든 바뀔 수 있습니다.
개요
지난 섹션들에서 우리는 신제품 문서를 자동으로 만드는 프로세스를 만들었어요. 이제 이 프로세스는 제품에 특화된 문서를 생성하고, 교정·수정 사이클을 거쳐 품질 기준을 통과하도록 만들 수 있습니다. 이번 섹션에서는 문서가 게시되기 전에 사람이 승인하거나 거부하도록 요구하는 단계를 추가해 프로세스를 한 번 더 개선해 볼게요. 프로세스 프레임워크가 유연하다 보니 이를 구현하는 방법도 여러 가지인데, 이 예시에서는 외부 pubsub 시스템과 연동해 승인을 요청하는 방식을 보여드릴게요.

게시가 승인을 기다리게 만들기
프로세스에 해줘야 할 첫 번째 변경은, 게시 단계가 문서를 게시하기 전에 승인을 기다리도록 만드는 것입니다. 방법 하나는 PublishDocumentationStep의 PublishDocumentation 함수에 승인용 파라미터를 하나 더 추가하는 거예요. Step 안의 KernelFunction은 필요한 파라미터가 모두 제공되어야만 호출되기 때문에, 이 방식이 잘 동작합니다.
::: zone pivot="programming-language-csharp"
// 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;
}
}
::: zone-end
::: zone pivot="programming-language-python"
Python Human-in-the-loop 프로세스 동작 지원은 곧 제공될 예정입니다. ::: zone-end
::: zone pivot="programming-language-java" ::: zone-end
위 코드로 PublishDocumentationStep의 PublishDocumentation 함수는, 생성된 문서가 document 파라미터로, 승인 결과가 userApproval 파라미터로 전달된 경우에만 호출됩니다.
이제 기존 ProofreadStep 로직을 재사용해 외부 pubsub 시스템으로 이벤트를 하나 더 발행해, 새로 승인 요청이 있다는 걸 인간 승인자에게 알려줄 수 있어요.
::: zone pivot="programming-language-csharp"
// 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);
}
...
}
::: zone-end
교정 에이전트가 승인한 새 문서를 게시하고 싶으니, 승인된 문서를 게시 단계에 큐로 쌓을 거예요. 게다가 최신 문서에 대한 업데이트를 외부 pubsub 시스템을 통해 사람에게 알려주죠. 이 새로운 설계에 맞게 프로세스 흐름을 갱신해 봅시다.
::: zone pivot="programming-language-python"
Python Human-in-the-loop 프로세스 동작 지원은 곧 제공될 예정입니다. ::: zone-end
::: zone pivot="programming-language-java" ::: zone-end
::: zone pivot="programming-language-csharp"
// 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;
::: zone-end
마지막으로, 새로운 ProxyStep이 내부적으로 사용하므로 IExternalKernelProcessMessageChannel 인터페이스의 구현체를 제공해야 합니다. 이 인터페이스는 메시지를 외부로 발행할 때 쓰여요. 구현체는 여러분이 사용하는 외부 시스템에 따라 달라집니다. 이 예시에서는 외부 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();
}
}
}
이제 프로세스의 ProxyStep이 IExternalKernelProcessMessageChannel 구현체, 여기서는 MyCloudEventClient를 사용하게 하려면 그것을 제대로 연결해 줘야 합니다.
Local Runtime을 쓸 때는 KernelProcess 클래스에서 StartAsync를 호출할 때 구현 클래스를 넘겨주면 됩니다.
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)
Dapr Runtime을 쓸 때는 프로젝트의 Program 설정에서 의존성 주입을 통해 연결을 구성해야 합니다.
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>();
::: zone-end
::: zone pivot="programming-language-python"
Python Human-in-the-loop 프로세스 동작 지원은 곧 제공될 예정입니다. ::: zone-end
::: zone pivot="programming-language-java" ::: zone-end
프로세스 흐름에 두 가지 변경이 생겼습니다.
docsPublishStep의userApproval파라미터로 라우팅될HumanApprovalResponse라는 입력 이벤트를 추가했어요.docsPublishStep의 KernelFunction이 이제 파라미터 두 개를 가지므로, 기존 라우트를 갱신해document라는 파라미터 이름을 명시해 줘야 합니다.
이전처럼 프로세스를 실행해 보세요. 이번에는 교정기가 생성된 문서를 승인해 docPublishStep의 document 파라미터로 보내도, 승인 단계가 userApproval 파라미터를 기다리느라 더는 호출되지 않는 걸 볼 수 있을 거예요. 이 시점에 실행할 준비가 된 Step이 없으므로 프로세스는 유휴(idle) 상태가 되고, 프로세스를 시작하려고 했던 호출은 반환됩니다. 프로세스는 우리 "사람 개입"이 게시 요청을 승인하거나 거부하기 전까지 이 유휴 상태에 머물러요. 사람의 결정이 나오고 그 결과가 프로그램으로 전달되면, 그 결과와 함께 프로세스를 다시 시작할 수 있습니다.
::: zone pivot="programming-language-csharp"
// Restart the process with approval for publishing the documentation.
await process.StartAsync(kernel, new KernelProcessEvent { Id = "UserApprovedDocument", Data = true });
::: zone-end
::: zone pivot="programming-language-python"
Python Human-in-the-loop 프로세스 동작 지원은 곧 제공될 예정입니다. ::: zone-end
::: zone pivot="programming-language-java" ::: zone-end
UserApprovedDocument로 프로세스를 다시 시작하면 멈춘 지점부터 이어서 docsPublishStep을 userApproval이 true인 상태로 호출하고, 문서가 게시됩니다. UserRejectedDocument 이벤트로 다시 시작하면 프로세스는 docsGenerationStep의 ApplySuggestions 함수를 시작하고 이전처럼 계속 진행돼요.
이제 프로세스가 완성됐습니다. 사람 개입 단계를 성공적으로 추가했네요. 이 프로세스로 우리 제품 문서를 만들고, 교정하고, 사람이 승인한 뒤에 게시할 수 있게 됐습니다.