AzureAICommunity.Agent.Middleware.ApprovalMiddleware
1.1.0
dotnet add package AzureAICommunity.Agent.Middleware.ApprovalMiddleware --version 1.1.0
NuGet\Install-Package AzureAICommunity.Agent.Middleware.ApprovalMiddleware -Version 1.1.0
<PackageReference Include="AzureAICommunity.Agent.Middleware.ApprovalMiddleware" Version="1.1.0" />
<PackageVersion Include="AzureAICommunity.Agent.Middleware.ApprovalMiddleware" Version="1.1.0" />
<PackageReference Include="AzureAICommunity.Agent.Middleware.ApprovalMiddleware" />
paket add AzureAICommunity.Agent.Middleware.ApprovalMiddleware --version 1.1.0
#r "nuget: AzureAICommunity.Agent.Middleware.ApprovalMiddleware, 1.1.0"
#:package AzureAICommunity.Agent.Middleware.ApprovalMiddleware@1.1.0
#addin nuget:?package=AzureAICommunity.Agent.Middleware.ApprovalMiddleware&version=1.1.0
#tool nuget:?package=AzureAICommunity.Agent.Middleware.ApprovalMiddleware&version=1.1.0
<div align="center">
๐ AzureAICommunity - Agent - Approval Middleware
Add a human-in-the-loop approval gate to AI agent tool calls with a single method call.
Getting Started ยท Callback Contract ยท How It Works ยท Contributing
</div>
Overview
AzureAICommunity.Agent.Middleware.ApprovalMiddleware adds an approval gate directly into the AIAgentBuilder function-invocation pipeline. You pass only the tools that need approval โ all other tools registered on the agent execute freely without interruption. Before any gated tool executes, your callback is called with the pending FunctionCallContent; you decide whether to approve or deny using any UI: console, desktop dialog, HTTP call to a remote approver, etc. The middleware itself contains no UI code.
โจ Features
| Feature | |
|---|---|
| ๐ฏ | Selective gating โ only the tools you specify are intercepted; others run freely |
| ๐ | Gate-all overload โ omit approvalTools to intercept every tool on the agent |
| ๐ | Simple callback โ Func<FunctionCallContent, Task<bool?>> receives the tool name and arguments |
| ๐ฌ | LLM-aware denial โ when denied, a descriptive message is returned to the model so it can reason about the refusal |
| โ๏ธ | Custom denial message โ optional denialMessageFactory to return per-call denial text to the LLM |
| ๐ | MEA integration โ drops directly into any Microsoft.Agents.AI pipeline via UseApprovalMiddleware() |
| ๐ฅ๏ธ | UI agnostic โ use any approval UI: console, GUI, HTTP, SignalR, webhooks |
| ๐ | Method chaining โ returns the builder for fluent composition with other middleware |
๐ฆ Installation
dotnet add package AzureAICommunity.Agent.Middleware.ApprovalMiddleware
๐ Quick Start
using AzureAICommunity.Agent.Middleware.ApprovalMiddleware;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
// Read-only tool โ no approval needed
var getStatusTool = AIFunctionFactory.Create(GetDeviceStatus);
// Destructive tools โ require approval before executing
var turnOnTool = AIFunctionFactory.Create(TurnOnDevice);
var turnOffTool = AIFunctionFactory.Create(TurnOffDevice);
// All tools are registered on the agent
AIAgent baseAgent = new ChatClientAgent(
chatClient,
instructions: "You are a helpful smart-home assistant.",
tools: [getStatusTool, turnOnTool, turnOffTool]);
// Only the destructive tools are passed to UseApprovalMiddleware.
// GetDeviceStatus is intentionally omitted โ it bypasses the approval gate.
AIAgent agent = new AIAgentBuilder(baseAgent)
.UseApprovalMiddleware(
[turnOnTool, turnOffTool],
async fc =>
{
// Return true โ approved
// Return false or null โ denied, a denial message is returned to the LLM
Console.Write($"Allow '{fc.Name}'? (y/n): ");
bool approved = Console.ReadLine()?.Trim() == "y";
return (bool?)approved;
},
denialMessageFactory: fc => fc.Name switch
{
"TurnOnDevice" => $"User refused to turn ON '{fc.Arguments?["deviceName"]}'. Do not retry.",
"TurnOffDevice" => $"User refused to turn OFF '{fc.Arguments?["deviceName"]}'. Do not retry.",
_ => $"User refused '{fc.Name}'. Do not retry this action."
})
.Build();
// Gate ALL tools โ no list needed
AIAgent agentGateAll = new AIAgentBuilder(baseAgent)
.UseApprovalMiddleware(async fc =>
{
Console.Write($"Allow '{fc.Name}'? (y/n): ");
return (bool?)(Console.ReadLine()?.Trim() == "y");
})
.Build();
AgentResponse response = await agent.RunAsync("Check the lights, then turn them on.");
Console.WriteLine(response);
๐ Callback Contract
The approvalCallback receives a FunctionCallContent with the tool name and arguments.
| Return value | Effect |
|---|---|
true |
Approved โ the tool executes and its real result is returned to the LLM |
false or null |
Denied โ a denial message is returned to the LLM as the tool result so it can reason about the refusal |
The callback is Func<FunctionCallContent, Task<bool?>>, so it can be async and use any UI or remote call.
โ๏ธ How It Works
LLM decides to call a tool
โโโบ FunctionInvocationContext intercepted by ApprovalMiddleware
โ
โโ gate-all mode OR tool name in approvalTools?
โ โโ YES โ approvalCallback(FunctionCallContent) called
โ โ โโ returns true โ next(context, ct) โ tool executes normally
โ โ โโ returns false/null โ denialMessageFactory(fc) โ text returned to LLM
โ โโ NO โ next(context, ct) โ tool executes freely, no approval prompt
โ
โโ agent continues with the result
๐ Type Reference
| Type | Description |
|---|---|
ApprovalMiddlewareExtensions |
Provides the UseApprovalMiddleware extension method on AIAgentBuilder |
FunctionCallContent |
Passed to the callback โ contains Name and Arguments of the pending tool call |
Extension methods
// Overload 1 โ gate specific tools
builder.UseApprovalMiddleware(
IEnumerable<AITool> approvalTools, // Tools that require approval
Func<FunctionCallContent, Task<bool?>> approvalCallback, // Approval decision callback
Func<FunctionCallContent, string>? denialMessageFactory = null // Optional: custom denial text
);
// Overload 2 โ gate ALL tools
builder.UseApprovalMiddleware(
Func<FunctionCallContent, Task<bool?>> approvalCallback, // Approval decision callback
Func<FunctionCallContent, string>? denialMessageFactory = null // Optional: custom denial text
);
denialMessageFactory
Optional factory that produces the text returned to the LLM as the tool result when a call is denied. Receives the FunctionCallContent of the denied call. If omitted, defaults to "User denied the call to '{name}'.".
denialMessageFactory: fc => fc.Name switch
{
"TurnOnDevice" => $"User refused to turn ON '{fc.Arguments?["deviceName"]}'. Do not retry.",
"TurnOffDevice" => $"User refused to turn OFF '{fc.Arguments?["deviceName"]}'. Do not retry.",
_ => $"User refused '{fc.Name}'. Do not retry this action."
}
๐ค Contributing
Contributions are welcome! Please open an issue to discuss what you'd like to change before submitting a pull request.
๐ Repository: https://github.com/rvinothrajendran/AgentFramework
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -m 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
๐ค Author
Built and maintained by Vinoth Rajendran.
- ๐ GitHub: github.com/rvinothrajendran โ follow for more projects!
- ๐บ YouTube: youtube.com/@VinothRajendran โ subscribe for tutorials and demos!
- ๐ผ LinkedIn: linkedin.com/in/rvinothrajendran โ let's connect!
๐ License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Agents.AI (>= 1.3.0)
- Microsoft.Extensions.AI (>= 10.5.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.