# Human In the Loop(HITL) in Azure Durable Functions for Microsoft Agent Framework

My previous [article](https://www.azureguru.net/multi-agent-orchestration-through-azure-durable-functions-in-microsoft-agent-framework) focused on Multi Agent Orchestration in Azure Durable Functions for Microsoft Agent Framework.

The use case used in the article was pretty straightforward wherein there were two agents .

*   Content generating agent
    
*   Content review agent
    

Content generated by the first agent is reviewed, modified and pushed forward for further processing by the second agent.

But such an straightforward approach might carry potential risks and might not be feasible in real life scenarios particularly for crucial business processes that adhere to strict compliances.

We wouldn't want agents to work autonomously and take decisions without proper human reviews . This is where **Human In The Loop (HITL)** plays an important role.

This article focusses on introducing **Human In The Loop (HITL)** concept for Azure Durable functions in MAF.

Also in the previous article, the user input was embedded in the code and there wasn't a mechanism to make the input dynamic . This article covers that aspect as well where the user input is dynamic and is sent while REST API invocation.

If you would like to skip the writeup you can view the flow walkthrough [here](https://youtu.be/dK2j7v-Is7Y)

**Flow >>**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9f21eabd-fa3f-4867-a906-37dff3e1baeb.png align="center")

The flow is pretty straightforward.

The entry point is an HTTP Trigger that accepts the input as **HttpRequestData** and creates an orchestration instance that invokes an Orchestration method called **RunOrchestrationAsync.**

In **RunOrchestrationAsync** method, the agent creates the content on the topic provided and a notification is sent to the user(human) to approve or reject it. In the meantime the flow pauses for the human response.

Once the **RunOrchestrationAsync** receives the response from the user(human), the method invokes the **PublishContent** activity if approved else invokes the **NotifyUseForRejection** activity if rejected . This concludes the orchestration execution.

### **SetUp**

To get started, create a new Azure Function project and apply settings covered in my previous [**article**](https://www.azureguru.net/azure-durable-agents-in-microsoft-agent-framework-with-docker-durable-task-scheduler-dts-emulator#setup) including setting up of the Docker DTS Emulator.

Ensure Docker DTS Emulator is up and navigate to [**http://localhost:8082/**](http://localhost:8082/) to ensure that it is running.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/15e6fdef-abc5-4086-80d5-4e3bd7477eec.png align="center")

### **Code**

After the above artifacts are in place, add the following code to read the settings from `appsettings.json` in **Program.cs** of the project.

```csharp
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
```

Read the credentials and register a `Chatclient`

```csharp
var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

ServiceCollection servicecollection = new();

builder.Services.AddKeyedChatClient(
    "ChatClient",
    (
        sp =>
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
                .AsIChatClient()
    )
);
```

In the next step, register the following agent in the hosted DI container.

**FootballContentCreatorAgent >>**

```csharp
servicecollection.AddSingleton<ChatClientAgent>(sp =>

{
    Func<ChatClientAgentOptions> func = () =>
   {
       return new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {
                Instructions = "You are a content creator.You are good at writing reviews for football club.Be concise and please stick to the topic.",
           },
           Name = "FootballContentCreatorAgent",
           Id = "1"
       };
   };

    return new ChatClientAgent(sp.GetKeyedService<IChatClient>("ChatClient"), options: func());

});
```

Build the service and fetch agents from the **ServiceProvider** as **ChatClientAgent.**

```csharp
ServiceProvider serviceProvider = servicecollection.BuildServiceProvider();

var agent = serviceProvider.GetServices<ChatClientAgent>();

List<ChatClientAgents> chatclientagent = new(agent);
```

Then add these agents as **DurableAgent** to the Azure Function worker.

```csharp
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(chatclientagent[0], timeToLive: TimeSpan.FromHours(1)))
.Build();

app.Run();
```

Create a new class file called **FunctionTigger.cs** and add two classes **Input** and **HumanApproval**.

**Input >>**

```csharp
public class Input
 {
   [JsonPropertyName("input")]
   public string input { get; set; }
 }
```

**HumanApproval>>**

```csharp
 public class HumanApproval
 {
     [JsonPropertyName("IsApproved")]
     public string IsApproved { get; set; }
 }
```

**Note** : I have kept the structure pretty simple. You can add additional properties based on your use case. For ex , a **Feedback** property in the **HumanApproval**  
class that accepts feedback from the human(user) prior to an approval/rejection.

Also add a **Record** object called **TextResponse** that accepts AgentResponses as input.

```csharp
 public record TextResponse(string Response);
```

**StartOrchestrationAsync >>**

As shown in the flowchart, **StartOrchestrationAsync** will be the method that acts as an entry point when the AzureFunction is triggered through an POST request to the HTTP endpoint.

```csharp
 [Function(nameof(StartOrchestrationAsync))]
 public static async Task<HttpResponseData> StartOrchestrationAsync(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/run")] HttpRequestData req,
    [DurableClient] DurableTaskClient client)
 {
     var input = await req.ReadFromJsonAsync<Input>();
     string instanceid = await client.ScheduleNewOrchestrationInstanceAsync(orchestratorName: nameof(RunOrchestrationAsync), input: input);

   var response = req.CreateResponse(System.Net.HttpStatusCode.Accepted);
    
   return response;
 }
```

The endpoint for the orchestration in the above example is **"hitl/run".**

A new orchestration instance is created in this method and which in turn invokes the **RunOrchestrationAsync** method.

**RunOrchestrationAsync >>**

```csharp
 [Function(nameof(RunOrchestrationAsync))]
public static async Task RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
    DurableAIAgent FootballContentCreatorAgent = context.GetAgent("FootballContentCreatorAgent");

    var input = context.GetInput<Input>();

    AgentSession Session = await FootballContentCreatorAgent.CreateSessionAsync();

    AgentResponse<TextResponse> content = await FootballContentCreatorAgent.RunAsync<TextResponse>(
        message: input.input,
        session: Session);

    TextResponse agentresponse = content.Result;

    await context.CallActivityAsync(nameof(NotifyUserForApproval), agentresponse);


    HumanApproval humanResponse;

    humanResponse = await context.WaitForExternalEvent<HumanApproval>(
          eventName: "HumanApproval",
          timeout: TimeSpan.FromHours(1));

    if (humanResponse.IsApproved == "Yes")
    {
        await context.CallActivityAsync(nameof(PublishContent), agentresponse);       
        context.SetCustomStatus($"Content published successfully at {context.CurrentUtcDateTime.ToLongTimeString}");            

    }
    else
    {
        await context.CallActivityAsync(nameof(NotifyUserForRejection), agentresponse);
        context.SetCustomStatus("Content is rejected by human reviewer. Publishing content...");

    }
  
}
```

**RunOrchestrationAsync** is method is the most important method that processes the user input.

Lets breakdown the above method :

We first create a new instance of **DurableAIAgent** from the context . The user input from the http endpoint is stored in the input variable of type **T** declared earlier.

```csharp
DurableAIAgent FootballContentCreatorAgent = context.GetAgent("FootballContentCreatorAgent");
var input = context.GetInput<Input>();
```

Create a new **AgentSession**. Executing the agent returns an output of type **AgentResponse**.

Then retrieve the underlying **TextResponse** from the **Result** property into the variable **agentresponse**.

```csharp
AgentSession Session = await FootballContentCreatorAgent.CreateSessionAsync();

AgentResponse<TextResponse> content = await FootballContentCreatorAgent.RunAsync<TextResponse>(
                message: input.input,
                session: Session);

TextResponse agentresponse = content.Result;
```

Execute **CallActivityAsync** that invokes a method named **NotifyUserForApproval** with agentresponse created above as the parameter to the method.

```csharp
await context.CallActivityAsync(nameof(NotifyUserForApproval), agentresponse);
```

Next, the method waits for humanapproval event through **WaitForExternalEvent** for a timespan of one hour.

```csharp
HumanApproval humanResponse = await context.WaitForExternalEvent<HumanApproval>(
eventName: "HumanApproval",
timeout: TimeSpan.FromHours(1));
```

Once the approval is approved/rejected the underlying methods are invoked through **CallActivityAsync** based on the type of input received.

```csharp
 if (humanResponse.IsApproved == "Yes")
 {
     await context.CallActivityAsync(nameof(PublishContent), agentresponse);    
     context.SetCustomStatus($"Content published successfully at {context.CurrentUtcDateTime.ToLongTimeString}");
 }
 else
 {
     await context.CallActivityAsync(nameof(NotifyUserForRejection), agentresponse);
     context.SetCustomStatus("Content is rejected by human reviewer. Publishing content...");
 }
```

**NotifyUserForApproval >>**

In real life use case an email or other forms of notifications with an link to the endpoint should be sent to for approval or rejections. This link is generated from the **HumanApprovalAsync** method explained later in the execution flow.

```csharp
 [Function(nameof(NotifyUserForApproval))]
 public async static Task<string> NotifyUserForApproval(
   [ActivityTrigger] TextResponse content,
   FunctionContext functionContext)
 {
     return $"Please review the following generated content{content}";
 }
```

**PublishContent >>**

```csharp
 [Function(nameof(PublishContent))]
 public async static Task<string> PublishContent(
[ActivityTrigger] TextResponse content,
FunctionContext functionContext)
 {
     return $"The following content {content} is approved and is published";
 }
```

**NotifyUserForRejection>>**

```csharp
 [Function(nameof(NotifyUserForRejection))]
 public async static Task<string> NotifyUserForRejection(
[ActivityTrigger] TextResponse content,
FunctionContext functionContext)
 {
     return $"The following content {content} is rejected ";
 }
```

**HumanApprovalAsync>>**

The approval/rejection endpoint with **hitl/notification/{instanceId}** is created in **HumanApprovalAsync** method and a response is returned with **IsApproved** value set based on the user input.

```csharp
 [Function(nameof(HumanApprovalAsync))]
 public static async Task<HttpResponseData> HumanApprovalAsync(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/notification/{instanceId}")] HttpRequestData req, string instanceId,
    [DurableClient] DurableTaskClient client)
 {
     var humanapproval = await req.ReadFromJsonAsync<HumanApproval>();
    
     await client.RaiseEventAsync(instanceId, "HumanApproval", humanapproval);

     HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);

     return response;
 }
```

You might ask how would the user approve/reject the request.

This can be done through the **HTTPost** that invokes the notification endpoints created in the **HumanApprovalAsync** method. Below is the PowerShell call to the endpoints.

**HTTP POST request to the StartOrchestrationAsync method >>**

```python
$body = @{
    input = "Write a review for the football club FC Barcelona"    
} | ConvertTo-Json

Invoke-RestMethod -Method Post `
    -Uri http://localhost:{Port no set in your launchsettings.json file}/api/hitl/run `
    -ContentType application/json `
    -Body $body
```

![HITL in Azure Durable Functions in MAF](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7d2278e5-5bf4-491d-9ee0-1945d03dd46e.png align="center")

**HTTP POST request to the HumanApprovalAsync method >>**

```python
$json = '{"IsApproved":"Yes"}'

Invoke-RestMethod `
-Uri "http://localhost:{Port no set in your launchsettings.json file}/api/hitl/notification/{InstanceId from StartOrchestrationAsync method}" `
-Method Post `
-ContentType "application/json" `
-Body $json
```

![HITL in Azure Durable Functions in MAF](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/122649d6-1865-4cdb-b641-026dd18153c5.png align="center")

**DTS Dashboard for User Approval >>**

![HITL in Azure Durable Functions in MAF](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/def1065f-82c4-4b0e-bce8-7b1a6e950ed7.png align="center")

**DTS Dashboard for User Rejection >>**

![HITL in Azure Durable Functions in MAF](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8864082e-025d-4f89-a7db-ac2512caa6be.png align="center")

### Execution

%[https://youtu.be/dK2j7v-Is7Y] 

### Conclusion

Through this article, I tried to explore a simple implementation of Human-in-the-Loop (HITL) for Azure Durable Functions.

Although the sample focuses on a basic approval workflow it highlights the core concepts required to pause an orchestration, wait for human input, and resume execution based on the outcome.

I hope this article helps you understand the fundamentals and sets the required foundation for building more business centric HITL solutions in your applications.

Thanks for reading !!!
