# Human-In-The-Loop (HITL) in Microsoft Agent Framework Workflow without ToolApprovalRequestContent 

The general approach for Human-In-The-Loop (HITL) in Microsoft Agent Framework is through **ToolApprovalRequestContent** driven by **AIFunction** invocation wrapped around **ApprovalRequiredAIFunction**.

But for MAF workflows it is possible to achieve HITL without the need for wrapping your **AIFunction** with **ApprovalRequiredAIFunction**.

In this article we will be exploring the alternate approach

If you would like to know more about HITL through **ApprovalRequiredAIFunction**, for Agents, then you can refer to my article on the topic [here](https://www.azureguru.net/human-in-the-loop-hitl-in-multi-agent-orchestration-in-microsoft-agent-framework) .

We will use the same use case that I used in my [article](https://www.azureguru.net/human-in-the-loop-hitl-in-multi-agent-orchestration-in-microsoft-agent-framework) on MAF Workflows to demonstrate how HITL can be leveraged without **ApprovalRequiredAIFunction**.

### **Use Case**

This was the flow for the use case used in my previous article on HITL

![Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/94395484-9843-4826-a886-cc3836d02086.png align="center")

So, we had an input number `N` that requires to be identified as a Prime or a non Prime number. If its a Prime number then compute its square root and send the value through **Route A** .

If its not a Prime number then send it through **Route B** without its square root.

Now , lets make some changes to the flow to accommodate HITL .

How about we add HITL approval before the value of √N is dispatched to Route A ?

So the flow would be as follows :

```csharp
                          typeDetectionExecutor
                                   |
           -----------------------------------------------
           |                       |                     |
           v                       v                     v
     PrimeNumber               NotPrimeNumber          Default
           |                       |                     |
           v                       v                     v
PrimeNo_SquareRootExecutor   Executor_RouteB    Unsure_Executor 
           |
           |
           v
      HITL Approval
           |
           |
           v
  -----------------
  |               |
Rejected       Approved
  |               |
  |               |
  v               v
Log Rejection Executor_RouteA
```

To achieve this we will use a combination of **RequestPort** and **RequestInfoEvent** .

**RequestPort & RequestInfoEvent >>**

RequestPort can be defined as a channel through which executors can send and receive responses. But you might ask that it is already possible to do that through **SendMessages** or **YieldsOutput** .

The drawback with both **SendMessages** or **YieldsOutput** is that they cant emit responses as Events.

> Recall that `SendMessages` sends messages to all the connected executors while `YieldsOutput` sends the output to the caller and both do not emit responses.

When an executor sends a message to RequestPort, it emits a RequestInfoEvent and then external inputs can listen to those RequestInfoEvent and then RequestInfoEvent sends it responses back to the workflow through RequestPort .

This is similar to **ToolApprovalRequestContent** but it requires an **AIFunction** wrapped in **ApprovalRequiredAIFunction** and **AIFunction** to be invoked through an agent.

With RequestPort, AIFunction invocation is not required as the workflow can read the input and receive output through RequestPort which is then emitted in form of RequestInfoEvent.

To implement this we will have to go back and make some changes to workflow edges and the **PrimeNo\_SendDetails\_RouteA\_Executor** that routes the square root value to RouteA.

Refer above flowchart and code for **PrimeNo\_SendDetails\_RouteA\_Executor** in my earlier article [here](https://www.azureguru.net/deep-dive-into-workflow-execution-in-microsoft-agent-framework).

### Code

First, add a new property named **IsApproved** to the **\_Response** object of the workflow.

**\_Response >>**

*Earlier version :*

```csharp
public sealed class _Response
{
    [JsonPropertyName("decision")]
    public string Decision { get; set; } = string.Empty;

    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;

    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("squareroot")]
    public double squareroot { get; set; } = 0.00;
}
```

*Changed version :*

```csharp
public sealed class _Response
{
    [JsonPropertyName("decision")]
    public string Decision { get; set; } = string.Empty;

    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;

    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;   
   
    [JsonPropertyName("squareroot")]
     public double squareroot { get; set; } = 0.00;
    
    [JsonPropertyName("approved")]
    public string IsApproved { get; set; } = string.Empty; 
}
```

**RequestPort >>**

```csharp
 RequestPort humanApprovalPort = RequestPort.Create<_Response, _Response>("Approval");
```

RequestPort instance is of type **T<TRequest,TResponse>**.

Output of **PrimeNo\_SquareRootExecutor** which is of type \_Response acts as **TRequest** for RequestPort and the output from RequestPort which is also of type \_Response acts as an input to **SendDetails\_Executor\_RouteA**.

**SendDetails\_Executor\_RouteA >>**

*Earlier version :*

```csharp
internal sealed class PrimeNo_SendDetails_RouteA_Executor() : Executor<_Response>("PrimeNo_SendDetails_RouteA_Executor")
{
    [YieldsOutput(typeof(string))]
    public override async ValueTask HandleAsync(_Response result, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        var number = await context.ReadStateAsync<InputNumber>(result.Id, scopeName: NumbervalueConstants.NumbervalueScope);
        await context.YieldOutputAsync($"Details for {number.Value} sent to Route A: {result.squareroot}", cancellationToken);

    }
}
```

*Changed version :*

```csharp
 internal sealed class PrimeNo_SendDetails_RouteA_Executor() : Executor<_Response>("PrimeNo_SendDetails_RouteA_Executor")
 {
     [YieldsOutput(typeof(string))]

     public override async ValueTask HandleAsync(_Response result, IWorkflowContext context, CancellationToken cancellationToken = default)
     {
         var number = await context.ReadStateAsync<InputNumber>(result.Id, scopeName: NumbervalueConstants.NumbervalueScope);

         if (result.IsApproved == "Yes")
         {
             await context.YieldOutputAsync($"Request is approved !!! Details for {number.Value} sent to Route A with square root value of: {result.squareroot}", cancellationToken);
         }
         else
         {
             await context.YieldOutputAsync($"Request is rejected !!! Details of rejection for {number.Value} will be logged ", cancellationToken);
         }
         
     }
 }
```

The change above, is that in the changed version , we are reading the **IsApproved** property value which was set through **RequestPort** and based on the output from approval/rejection , a decision is made to route the square root value or log the rejection.

But, how to set the value for **IsApproved** property ?

This is done through **ExternalResponse** process where the executor pauses its execution awaiting an input from external system. But then you might wonder how to invoke the **ExternalResponse** process.This is done by adding the **RequestPort** as part of the workflow Edge.

A method named **HandleExternalRequest** is defined later in the article to that handles **ExternalResponse**.

Recall that earlier we declared RequestPort with name **humanApprovalPort**.

**Edges >>**

*Earlier version :*

```csharp
.AddEdge(PrimeNo_SquareRootExecutor,SendDetails_Executor_RouteA)
.WithOutputFrom(SendDetails_Executor_RouteA, SendDetails_Executor_RouteB, SendEmail_Executor_Unsure);
```

*Changed version :*

```csharp
.AddEdge(PrimeNo_SquareRootExecutor, humanApprovalPort)
.AddEdge(humanApprovalPort, SendDetails_Executor_RouteA)
.WithOutputFrom(SendDetails_Executor_RouteA, SendDetails_Executor_RouteB, SendEmail_Executor_Unsure);
```

In the workflow event we then invoke **ExternalResponse** that returns a response object which is then sent to the Executor.

**WorkflowEvent>>**

*Earlier version :*

```csharp
var workflow = builder.Build();

string input = "Your Input Number/Letter";

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, input));

  await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
  {
    if (evt is WorkflowOutputEvent outputEvent)
    {
     Console.WriteLine($"{outputEvent}");
    }
  }
```

*Changed version :*

```csharp
var workflow = builder.Build();

string input = "Your Input Number/Letter";

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, input));

  await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
  {
    if (evt is WorkflowOutputEvent outputEvent)
    {
     Console.WriteLine($"{outputEvent}");
    }    
    
    if (evt is RequestInfoEvent requestInputEvt)
    {
     ExternalResponse response =  HandleExternalRequest(requestInputEvt.Request);
   await run.SendResponseAsync(response);
   }
 
  }
```

**HandleExternalRequest >>**

```csharp
 private static ExternalResponse HandleExternalRequest(ExternalRequest request)
 {
     if (request.TryGetDataAs<_Response>(out var response))
     {

         Console.WriteLine($"Would you like to approve/reject ? please reply Y to approve and N to reject");

         response.IsApproved = Convert.ToString(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) == true ? "Yes" : "No");

         return request.CreateResponse(response);
     }

     throw new NotSupportedException($"Request {request.PortInfo.RequestType} is not supported");
 }
```

As the output from **PrimeNo\_SquareRootExecutor** is of type \_Response, the \_Response object becomes a part of ExternalRequest which in turn exposes the IsApproved property through the out variable **response** and post human interaction , the modified request is sent back as a response.

**Execution >>**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c86f3c67-313c-4fb6-997e-03a54bc36acb.gif align="center")

### Conclusion

Through this article I tried to explore a more unique approach where HITL does not necessarily had to be dependent on AIFunction invocation through an AIAgent.

Instead, we saw how human approval can be introduced at different stages of an workflow through simple call mechanism through RequestPort and the responses can be handled through ExternalResponse object.

Go ahead and give it a try.

Thanks for reading !!!
