DevExpress Blazor AI Chat — Tool Call Confirmation
Modern AI-powered applications often execute tools automatically in response to user queries. While this automation embraces the potential of LLMs and improves user experiences, it can introduce security risks when sensitive operations are invoked without explicit user consent — for example, modifying databases, sending emails, or making API calls to external services.
This post outlines the purpose of our Tool Call Confirmation API layer and associated customizable interface for the DevExpress Blazor AI Chat component. As you would expect, our solution intercepts AI-initiated function calls, generates detailed confirmation dialogs, and requires user approval before execution. This UI pattern is common in GitHub Copilot Chat, Cursor, Claude, and other AI-powered applications with MCP support.
In this post, I'll walk you through our AI Chat confirmation system, its key components, and customization options (and of course show you how to add this security layer to your AI-powered Blazor applications using the DevExpress Blazor AI Chat control).
The Challenge: Balancing Automation and Security
AI function calling is a powerful resource - one that allows models to interact seamlessly with app functionality. However, this power must be used responsibly. Consider the following usage scenarios:
- An AI model deletes user data in response to an ambiguous request.
- Unmanaged API calls generate unexpected costs or trigger external processes.
- Functions modify application state or UI without the user's knowledge.
The technique described in this post strikes a balance between convenience and control: it preserves the benefits of automated tool calling and gives users explicit authority over sensitive or irreversible operations.
Before You Start
Create and configure the DevExpress Blazor Chat ( DxAIChat ) component in your Blazor application. For setup instructions, review the following: Blazor Chat documentation.
This example targets Azure OpenAI, but the solution is compatible with any AI service that implements the IChatClient interface from the "Microsoft.Extensions.AI" library.
The following code in Program.cs defines the basic configuration:
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
var builder = WebApplication.CreateBuilder(args);
// Replace with your endpoint, API key, and model's deployment name
string azureOpenAIEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
string azureOpenAIKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
string deploymentName = "your-model-deployment-name";
var azureChatClient = new AzureOpenAIClient(
new Uri(azureOpenAIEndpoint),
new AzureKeyCredential(azureOpenAIKey))
.GetChatClient(deploymentName)
.AsIChatClient();
builder.Services.AddDevExpressBlazor();
builder.Services.AddDevExpressAI();
Architecture Overview
-
IToolCallFilter
Manages AI tool calls before they are executed. -
CustomFunctionInvokingChatClient
Extends the default behavior used to invokes tools. -
CustomFunctionInvokingChatClientExtensions
The Fluent API allows you to leverage tool call confirmation with a single line of configuration when building the chat client. -
Confirmation UI
Displays the dialog used to confirm tool calls.
Manage Tool Calls (IToolCallFilter)
The heart of our confirmation system is the IToolCallFilter interface - an interface which defines how the chat intercepts and manages function calls.
public interface IToolCallFilter {
public event Action<FunctionInvocationContext, TaskCompletionSource<bool>> ToolCalled;
Task<bool> InvokeFunctionFilter(FunctionInvocationContext context);
}
The MyToolCallFilter manages AI-triggered actions and executes sensitive operations only when approved. The API design allows the filter to pause function execution, await user input, and continue or cancel based on the user's decision. A TaskCompletionSource<bool> instance enables asynchronous communication between the UI and filter logic.
public class MyToolCallFilter : IToolCallFilter {
public event Action<FunctionInvocationContext, TaskCompletionSource<bool>> ToolCalled;
public Task<bool> InvokeFunctionFilter(FunctionInvocationContext context) {
if (ToolCalled is null)
return Task.FromResult(true);
var tcs = new TaskCompletionSource<bool>();
ToolCalled.Invoke(context, tcs);
return tcs.Task;
}
}
Confirm Tool Calls (CustomFunctionInvokingChatClient)
The CustomFunctionInvokingChatClient class extends our Blazor AI Chat control's default behavior:
-
Intercepts AI tool calls (defines a custom
FunctionInvokerdelegate). - Checks for confirmation logic (whether the filter has been registered in the service collection).
-
Proceeds with user approval. If the user cancels the request, the
CustomFunctionInvokermethod call returns a cancellation message instead of executing the tool. The message tells the LLM that the operation was aborted and that the requested information is unavailable.
public class CustomFunctionInvokingChatClient : FunctionInvokingChatClient {
public CustomFunctionInvokingChatClient(IChatClient innerClient, ILoggerFactory? factory = null,
IServiceProvider? services = null)
: base(innerClient, factory, services) {
if(services == null) {
throw new ArgumentNullException(nameof(services), "Service provider cannot be null.");
}
FunctionInvoker = CustomFunctionInvoker;
}
private async ValueTask<object?> CustomFunctionInvoker(FunctionInvocationContext context,
CancellationToken cancellationToken) {
IToolCallFilter? filter = FunctionInvocationServices!.GetService<IToolCallFilter>();
if(await (filter?.InvokeFunctionFilter(context) ?? Task.FromResult(true))) {
return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
}
return "The tool call was cancelled by the user. Do not attempt to invoke this tool again. Return a message indicating that the call was cancelled and that the weather information is unavailable at this time.";
}
}
Create the Confirmation UI
The confirmation dialog displays details about the pending tool call, including tool name, description, and arguments. A user can verify the tool call and ensure that retrieved arguments match the request. Confirm and Cancel buttons allow the user to approve or cancel the operation.
@if(_pendingTcs != null) {
<div>
@if(_pendingContext != null) {
<p><strong>Please confirm the tool call.</strong></p>
<blockquote>
<p><strong>Tool Called:</strong> @_pendingContext.Function.Name</p>
<p><strong>Description:</strong> @_pendingContext.Function.Description</p>
</blockquote>
<blockquote>
<strong>Arguments:</strong>
<ul>
@foreach(var arg in _pendingContext.Arguments) {
<li><strong>@arg.Key</strong>: @arg.Value</li>
}
</ul>
</blockquote>
}
<DxButton Text="Confirm"
RenderStyle="ButtonRenderStyle.Success"
IconCssClass="oi oi-check"
Click="() => OnDecisionMade(true)" />
<DxButton Text="Cancel"
RenderStyle="ButtonRenderStyle.Secondary"
IconCssClass="oi oi-x"
Click="() => OnDecisionMade(false)" />
</div>
}
The following code implements confirmation workflow. The ConfirmationButtons component subscribes to the ToolCalled event exposed by IToolCallFilter. When AI Chat attempts to invoke a tool, the filter fires this event and passes in a FunctionInvocationContext and a TaskCompletionSource<bool> that awaits the user's decision.
@code {
private FunctionInvocationContext? _pendingContext;
private TaskCompletionSource<bool>? _pendingTcs;
[Inject] IToolCallFilter? ToolCallFilter { get; set; }
protected override void OnInitialized() {
if(ToolCallFilter != null) {
ToolCallFilter.ToolCalled += OnFunctionInvoked;
}
}
private void OnFunctionInvoked(FunctionInvocationContext context, TaskCompletionSource<bool> tcs) {
_pendingContext = context;
_pendingTcs = tcs;
StateHasChanged();
}
private void OnDecisionMade(bool decision) {
_pendingTcs!.SetResult(decision);
_pendingContext = null;
_pendingTcs = null;
}
public void Dispose() {
if(ToolCallFilter != null) {
ToolCallFilter.ToolCalled -= OnFunctionInvoked;
}
}
}
Integrate Confirmation UI with Blazor AI Chat
The chat displays the confirmation dialog whenever the LLM is about to execute a tool. The MessageContentTemplate renders the confirmation dialog while the chat is in a "typing" state (indicating that the tool call is being processed).
<DxAIChat CssClass="main-content">
<MessageContentTemplate Context="context">
@context.Content
@if(context.Typing) {
<ConfirmationButtons />
}
</MessageContentTemplate>
</DxAIChat>
Register Services
In Program.cs, register MyToolCallFilter and IChatClient within the dependency injection (DI) container for each user session:
// Register the tool call filter
builder.Services.AddScoped<IToolCallFilter, MyToolCallFilter>();
// Configure the chat client with the confirmation layer
builder.Services.AddScoped(x => {
return new ChatClientBuilder(azureChatClient)
.ConfigureOptions(x => {
x.Tools = [CustomAIFunctions.GetWeatherTool];
})
.UseMyToolCallConfirmation()
.Build(x);
});
builder.Services.AddDevExpressAI();
Fluent API Extension
A fluent API extension allows you to activate tool call confirmation with a single method call in your chat client configuration:
public static class CustomFunctionInvokingChatClientExtensions {
public static ChatClientBuilder UseMyToolCallConfirmation(this ChatClientBuilder builder,
ILoggerFactory? loggerFactory = null) {
return builder.Use((innerClient, services) => {
loggerFactory ??= services.GetService<ILoggerFactory>();
return new CustomFunctionInvokingChatClient(innerClient, loggerFactory, services);
});
}
}
User Experience in Action
When a user requests weather information, the following sequence occurs:
- The LLM identifies that a function call is required.
- A confirmation dialog is displayed (instead of executing immediately).
- The confirmation dialog displays tool name, description, and retrieved arguments.
- The user can approve or cancel the operation
- Only approved tools are executed (their results are returned to the LLM).
Advanced Customization Options
Selective Tool Call Confirmation
SelectiveToolCallFilter extends IToolCallFilter to require user confirmation before calling tools designed to execute sensitive operations (such as deleting data, sending emails, or processing payments):
public class SelectiveToolCallFilter : IToolCallFilter {
private readonly string[] _sensitiveOperations = { "DeleteData", "SendEmail", "ChargePayment" };
public Task<bool> InvokeFunctionFilter(FunctionInvocationContext context) {
// Only require confirmation for sensitive operations
if (!_sensitiveOperations.Contains(context.Function.Name)) {
return Task.FromResult(true);
}
// Show confirmation for sensitive operations
if (ToolCalled is null)
return Task.FromResult(true);
var tcs = new TaskCompletionSource<bool>();
ToolCalled.Invoke(context, tcs);
return tcs.Task;
}
}
Enhanced Confirmation UI
Modify the ConfirmationButtons.razor component to create a confirmation dialog based on UI requirements. You can modify the layout, apply custom styles, and inform users about the tool they are about to execute.
<div class="tool-confirmation-container">
<div class="confirmation-header">
<h5>⚠️ Function Execution Request</h5>
</div>
<div class="function-details">
<div class="detail-row">
<span class="label">Function:</span>
<span class="value">@_pendingContext.Function.Name</span>
</div>
<div class="detail-row">
<span class="label">Description:</span>
<span class="value">@_pendingContext.Function.Description</span>
</div>
</div>
<div class="arguments-section">
<h6>Arguments:</h6>
@foreach(var arg in _pendingContext.Arguments) {
<div class="argument-item">
<strong>@arg.Key:</strong> @arg.Value
</div>
}
</div>
<div class="action-buttons">
<DxButton Text="Approve"
RenderStyle="ButtonRenderStyle.Success"
Click="() => OnDecisionMade(true)" />
<DxButton Text="Deny"
RenderStyle="ButtonRenderStyle.Danger"
Click="() => OnDecisionMade(false)" />
</div>
</div>
Audit Trail
Log user decisions with relevant details (such as tool name, function arguments, timestamp, and user role):
public class AuditingToolCallFilter : IToolCallFilter {
private readonly ILogger<AuditingToolCallFilter> _logger;
public AuditingToolCallFilter(ILogger<AuditingToolCallFilter> logger) {
_logger = logger;
}
public event Action<FunctionInvocationContext, TaskCompletionSource<bool>>? ToolCalled;
public Task<bool> InvokeFunctionFilter(FunctionInvocationContext context)
{
if (ToolCalled is null)
return Task.FromResult(true);
var tcs = new TaskCompletionSource<bool>();
// Subscribe to the task completion to log the decision
tcs.Task.ContinueWith(task => {
if (task.IsCompletedSuccessfully)
{
OnDecisionMade(task.Result, context);
}
}, TaskContinuationOptions.ExecuteSynchronously);
ToolCalled.Invoke(context, tcs);
return tcs.Task;
}
private void OnDecisionMade(bool decision, FunctionInvocationContext context) {
_logger.LogInformation("User {Decision} function call: {FunctionName} with args: {Arguments}",
decision ? "approved" : "denied",
context.Function.Name,
string.Join(", ", context.Arguments.Select(a => $"{a.Key}={a.Value}")));
// Send decision data to the analytics service
// TrackUserDecision(decision, context);
}
}
Security Considerations
The confirmation UI enhances function calling transparency and user control. To strengthen security alongside the confirmation UI, implement the following measures:
- Rate Limiting: Restrict the number of tool calls per user or session.
- Permission Checks: Verify user permissions before displaying the confirmation dialog.
- Function Validation: Validate function arguments before execution.
- Audit Trails: Log all tool calls and user decisions for monitoring and compliance.
- Timeout Handling: Automatically deny tool execution if the user does not respond within a predefined period of time.
Download the Example
The full source code for this implementation is available on GitHub. Clone the repo and follow the instructions to setup your tool call confirmation system.
Your Feedback Counts!
This tool call confirmation system demonstrates one approach to balancing AI automation with user control. We're considering including similar functionality as a built-in feature in future versions of the DevExpress Blazor AI Chat component.
In the meantime, we'd love to hear your thoughts on this implementation: