Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>

<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MEAI001</NoWarn>

<!--
Disable central package management for this project.
This project requires explicit package references with versions specified inline rather than
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
be created from this project, and the Docker build process only has access to this folder
and cannot access parent folders where Directory.Packages.props resides.
-->
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>

<!--
Remove analyzer PackageReference items inherited from Directory.Packages.props.
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
and the PackageReference items are always inherited through MSBuild imports regardless of the
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
-->
<ItemGroup>
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
<PackageReference Remove="xunit.analyzers" />
<PackageReference Remove="Moq.Analyzers" />
<PackageReference Remove="Roslynator.Analyzers" />
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.8" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
</ItemGroup>

<!-- Add analyzers with compatible versions -->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Build the application
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src

# Copy files from the current directory on the host to the working directory in the container
COPY . .

RUN dotnet restore
RUN dotnet build -c Release --no-restore
RUN dotnet publish -c Release --no-build -o /app -f net10.0

# Run the application
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app

# Copy everything needed to run the app from the "build" stage.
COPY --from=build /app .

EXPOSE 8088
ENTRYPOINT ["dotnet", "AgentThreadAndHITL.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.

// This sample demonstrates Human-in-the-Loop (HITL) capabilities with thread persistence.
// The agent wraps function tools with ApprovalRequiredAIFunction to require user approval
// before invoking them. Users respond with 'approve' or 'reject' when prompted.

using System.ComponentModel;
using Azure.AI.AgentServer.AgentFramework.Extensions;
using Azure.AI.AgentServer.AgentFramework.Persistence;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";

[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";

// Create the chat client and agent.
// Note: ApprovalRequiredAIFunction wraps the tool to require user approval before invocation.
// User should reply with 'approve' or 'reject' when prompted.
#pragma warning disable MEAI001 // Type is for evaluation purposes only
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.AsIChatClient()
.CreateAIAgent(
instructions: "You are a helpful assistant",
tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]
);
#pragma warning restore MEAI001

var threadRepository = new InMemoryAgentThreadRepository(agent);
await agent.RunAIAgentAsync(telemetrySourceName: "Agents", threadRepository: threadRepository);
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# What this sample demonstrates

This sample demonstrates Human-in-the-Loop (HITL) capabilities with thread persistence. The agent wraps function tools with `ApprovalRequiredAIFunction` so that every tool invocation requires explicit user approval before execution. Thread state is maintained across requests using `InMemoryAgentThreadRepository`.

Key features:
- Requiring human approval before executing function calls
- Persisting conversation threads across multiple requests
- Approving or rejecting tool invocations at runtime

> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md).

## Prerequisites

Before running this sample, ensure you have:

1. .NET 10 SDK installed
2. An Azure OpenAI endpoint configured
3. A deployment of a chat model (e.g., gpt-4o-mini)
4. Azure CLI installed and authenticated (`az login`)

## Environment Variables

Set the following environment variables:

```powershell
# Replace with your Azure OpenAI endpoint
$env:AZURE_OPENAI_ENDPOINT="https://your-openai-resource.openai.azure.com/"

# Optional, defaults to gpt-4o-mini
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```

## How It Works

The sample uses `ApprovalRequiredAIFunction` to wrap standard AI function tools. When the model decides to call a tool, the wrapper intercepts the invocation and returns a HITL approval request to the caller instead of executing the function immediately.

1. The user sends a message (e.g., "What is the weather in Vancouver?")
2. The model determines a function call is needed and selects the `GetWeather` tool
3. `ApprovalRequiredAIFunction` intercepts the call and returns an approval request containing the function name and arguments
4. The user responds with `approve` or `reject`
5. If approved, the function executes and the model generates a response using the result
6. If rejected, the model generates a response without the function result

Thread persistence is handled by `InMemoryAgentThreadRepository`, which stores conversation history keyed by `conversation.id`. This means the HITL flow works across multiple HTTP requests as long as each request includes the same `conversation.id`.

> **Note:** HITL requires a stable `conversation.id` in every request so the agent can correlate the approval response with the original function call. Use the `run-requests.http` file in this directory to test the full approval flow.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: AgentThreadAndHITL
displayName: "Weather Assistant Agent"
description: >
A Weather Assistant Agent that provides weather information and forecasts. It
demonstrates how to use Azure AI AgentServer with Human-in-the-Loop (HITL)
capabilities to get human approval for functional calls.
metadata:
authors:
- Microsoft Agent Framework Team
tags:
- Azure AI AgentServer
- Microsoft Agent Framework
- Human-in-the-Loop
template:
kind: hosted
name: AgentThreadAndHITL
protocols:
- protocol: responses
version: v1
environment_variables:
- name: AZURE_OPENAI_ENDPOINT
value: ${AZURE_OPENAI_ENDPOINT}
- name: AZURE_OPENAI_DEPLOYMENT_NAME
value: gpt-4o-mini
resources:
- name: "gpt-4o-mini"
kind: model
id: gpt-4o-mini
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@host = http://localhost:8088
@endpoint = {{host}}/responses

### Health Check
GET {{host}}/readiness

###
# HITL (Human-in-the-Loop) Flow
#
# This sample requires a multi-turn conversation to demonstrate the approval flow:
# 1. Send a request that triggers a tool call (e.g., asking about the weather)
# 2. The agent responds with a function_call named "__hosted_agent_adapter_hitl__"
# containing the call_id and the tool details
# 3. Send a follow-up request with a function_call_output to approve or reject
#
# IMPORTANT: You must use the same conversation.id across all requests in a flow,
# and update the call_id from step 2 into step 3.
###

### Step 1: Send initial request (triggers HITL approval)
# @name initialRequest
POST {{endpoint}}
Content-Type: application/json

{
"input": "What is the weather like in Vancouver?",
"stream": false,
"conversation": {
"id": "conv_test0000000000000000000000000000000000000000000000"
}
}

### Step 2: Approve the function call
# Copy the call_id from the Step 1 response output and replace below.
# The response will contain: "name": "__hosted_agent_adapter_hitl__" with a "call_id" value.
POST {{endpoint}}
Content-Type: application/json

{
"input": [
{
"type": "function_call_output",
"call_id": "REPLACE_WITH_CALL_ID_FROM_STEP_1",
"output": "approve"
}
],
"stream": false,
"conversation": {
"id": "conv_test0000000000000000000000000000000000000000000000"
}
}

### Step 3 (alternative): Reject the function call
# Use this instead of Step 2 to deny the tool execution.
POST {{endpoint}}
Content-Type: application/json

{
"input": [
{
"type": "function_call_output",
"call_id": "REPLACE_WITH_CALL_ID_FROM_STEP_1",
"output": "reject"
}
],
"stream": false,
"conversation": {
"id": "conv_test0000000000000000000000000000000000000000000000"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Key features:
- Filtering available tools from an MCP server
- Using Azure OpenAI Responses with MCP tools

> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md).

## Prerequisites

Before running this sample, ensure you have:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>

<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnablePreviewFeatures>true</EnablePreviewFeatures>

<!--
Disable central package management for this project.
This project requires explicit package references with versions specified inline rather than
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
be created from this project, and the Docker build process only has access to this folder
and cannot access parent folders where Directory.Packages.props resides.
-->
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>

<!--
Remove analyzer PackageReference items inherited from Directory.Packages.props.
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
and the PackageReference items are always inherited through MSBuild imports regardless of the
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
-->
<ItemGroup>
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
<PackageReference Remove="xunit.analyzers" />
<PackageReference Remove="Moq.Analyzers" />
<PackageReference Remove="Roslynator.Analyzers" />
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.8" />
<PackageReference Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
</ItemGroup>

<!-- Add analyzers with compatible versions -->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Loading
Loading