File: Blocks\FunctionApprovalBlock.cs
Web Access
Project: src\aspnetcore\src\Components\AI\src\Microsoft.AspNetCore.Components.AI.csproj (Microsoft.AspNetCore.Components.AI)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Extensions.AI;
 
namespace Microsoft.AspNetCore.Components.AI;
 
/// <summary>
/// Represents a function invocation that requires approval before the conversation can continue.
/// </summary>
/// <example>
/// <code>
/// if (block.Status == ApprovalStatus.Pending)
/// {
///     block.Approve();
/// }
/// </code>
/// </example>
public class FunctionApprovalBlock : ContentBlock, IInteractiveBlock
{
    private readonly TaskCompletionSource<AIContent> _resultSource =
        new(TaskCreationOptions.RunContinuationsAsynchronously);
    private readonly object _responseLock = new();
 
    internal FunctionApprovalBlock(
        ContentBlock innerBlock,
        ToolApprovalRequestContent request)
    {
        ArgumentNullException.ThrowIfNull(innerBlock);
        InnerBlock = innerBlock;
        ApprovalRequest = request;
    }
 
    /// <summary>
    /// Gets the wrapped function invocation block.
    /// </summary>
    public ContentBlock InnerBlock { get; }
 
    /// <summary>
    /// Gets the function call represented by this block.
    /// </summary>
    public FunctionCallContent? Call => InnerBlock switch
    {
        FunctionInvocationContentBlock invocation => invocation.Call,
        UIActionBlock action => action.Call,
        _ => null,
    };
 
    /// <summary>
    /// Gets the function result represented by this block.
    /// </summary>
    public FunctionResultContent? Result => InnerBlock switch
    {
        FunctionInvocationContentBlock invocation => invocation.Result,
        UIActionBlock action => action.Result,
        _ => null,
    };
 
    /// <summary>
    /// Gets the name of the invoked tool.
    /// </summary>
    public string? ToolName => Call?.Name;
 
    /// <summary>
    /// Gets the arguments supplied to the tool.
    /// </summary>
    public IDictionary<string, object?>? Arguments => Call?.Arguments;
 
    /// <summary>
    /// Gets a value indicating whether the server produced a result.
    /// </summary>
    public bool HasResult => Result is not null;
 
    /// <summary>
    /// Gets the current approval status.
    /// </summary>
    public ApprovalStatus Status { get; private set; }
 
    /// <summary>
    /// Gets the approval request received from the chat client.
    /// </summary>
    public ToolApprovalRequestContent ApprovalRequest { get; }
 
    /// <summary>
    /// Approves the function invocation. Only the first response is applied.
    /// </summary>
    public void Approve()
    {
        Respond(ApprovalStatus.Approved, reason: null);
    }
 
    /// <summary>
    /// Rejects the function invocation. Only the first response is applied.
    /// </summary>
    /// <param name="reason">An optional explanation for the rejection.</param>
    public void Reject(string? reason = null)
    {
        Respond(ApprovalStatus.Rejected, reason);
    }
 
    /// <inheritdoc />
    public Task<AIContent> GetResultAsync(CancellationToken cancellationToken = default)
        => _resultSource.Task.WaitAsync(cancellationToken);
 
    private void Respond(ApprovalStatus status, string? reason)
    {
        lock (_responseLock)
        {
            if (Status != ApprovalStatus.Pending)
            {
                return;
            }
 
            Status = status;
            var response = ApprovalRequest.CreateResponse(
                approved: status == ApprovalStatus.Approved,
                reason);
            _resultSource.SetResult(response);
        }
 
        NotifyChanged();
    }
}