File: ChatRouting\RoutingChatClient.cs
Project: ..\..\..\src\Libraries\Microsoft.Extensions.AI.Abstractions\Microsoft.Extensions.AI.Abstractions.csproj (Microsoft.Extensions.AI.Abstractions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
 
namespace Microsoft.Extensions.AI;
 
/// <summary>
/// Provides a template for an <see cref="IChatClient"/> that selects and invokes another chat client.
/// </summary>
/// <remarks>
/// <para>
/// Derived classes implement <see cref="SelectClientAsync"/> to supply one client for each request. The selected
/// client is invoked once, and its response or failure is propagated to the caller.
/// The exact client instance represents the selected routing identity. Caller-supplied options may vary per
/// invocation, but they are ephemeral and do not participate in that identity. Applications can use distinct
/// configured client wrappers when configurations require distinct routing identities, while custom policies may
/// maintain separate application-specific grouping keys.
/// </para>
/// <para>
/// <see cref="RoutingContext.ChatOptions"/> holds the options for the request and is what the selected client
/// receives. Options that belong to a particular route are configured on the client instead, typically with a
/// <c>ConfigureOptionsChatClient</c> wrapper, which clones the request options and applies the route's own values
/// on top of them.
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIRoutingChat, UrlFormat = DiagnosticIds.UrlFormat)]
public abstract class RoutingChatClient : IChatClient
{
    /// <summary>Creates a routing client that selects one client for each request.</summary>
    /// <param name="clientSelector">The callback that selects the client to invoke.</param>
    /// <returns>A routing client that uses <paramref name="clientSelector"/>.</returns>
    /// <remarks>The selected clients are caller-owned and are not disposed by the returned routing client.</remarks>
    /// <exception cref="ArgumentNullException"><paramref name="clientSelector"/> is <see langword="null"/>.</exception>
    public static RoutingChatClient Create(
        Func<RoutingContext, CancellationToken, ValueTask<IChatClient>> clientSelector)
    {
        _ = Throw.IfNull(clientSelector);
 
        return new CallbackRoutingChatClient(clientSelector);
    }
 
    /// <summary>Selects the client to invoke for the request.</summary>
    /// <param name="context">The request-specific inputs.</param>
    /// <param name="cancellationToken">The cancellation token supplied for the request.</param>
    /// <returns>The client to invoke.</returns>
    /// <remarks>
    /// Client-specific behavior should generally be attached to the returned client. Modifying
    /// <see cref="RoutingContext.ChatOptions"/> changes the options for the request itself, including any later
    /// invocation of a different client for the same request. Exceptions from this method propagate to the caller.
    /// </remarks>
    protected abstract ValueTask<IChatClient> SelectClientAsync(
        RoutingContext context,
        CancellationToken cancellationToken);
 
    /// <inheritdoc/>
    public virtual async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
    {
        _ = Throw.IfNull(messages);
 
        var context = new RoutingContext(messages, options);
        IChatClient client = await SelectClientAsync(context, cancellationToken).ConfigureAwait(false) ??
            throw new InvalidOperationException($"{nameof(SelectClientAsync)} returned null.");
 
        return await client.GetResponseAsync(
            context.Messages,
            context.ChatOptions,
            cancellationToken).ConfigureAwait(false);
    }
 
    /// <inheritdoc/>
    public virtual async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        _ = Throw.IfNull(messages);
 
        var context = new RoutingContext(messages, options);
        IChatClient client = await SelectClientAsync(context, cancellationToken).ConfigureAwait(false) ??
            throw new InvalidOperationException($"{nameof(SelectClientAsync)} returned null.");
 
        await foreach (ChatResponseUpdate update in
            client.GetStreamingResponseAsync(context.Messages, context.ChatOptions, cancellationToken)
                .WithCancellation(cancellationToken)
                .ConfigureAwait(false))
        {
            yield return update;
        }
    }
 
    /// <inheritdoc/>
    public virtual object? GetService(Type serviceType, object? serviceKey = null)
    {
        _ = Throw.IfNull(serviceType);
 
        return serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null;
    }
 
    /// <inheritdoc/>
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
 
    /// <summary>Provides a mechanism for releasing resources owned by the derived instance.</summary>
    /// <param name="disposing"><see langword="true"/> when called from <see cref="Dispose()"/>.</param>
    /// <remarks>The default implementation performs no operation.</remarks>
    protected virtual void Dispose(bool disposing)
    {
    }
 
    private sealed class CallbackRoutingChatClient : RoutingChatClient
    {
        private readonly Func<RoutingContext, CancellationToken, ValueTask<IChatClient>> _clientSelector;
 
        public CallbackRoutingChatClient(
            Func<RoutingContext, CancellationToken, ValueTask<IChatClient>> clientSelector)
        {
            _clientSelector = clientSelector;
        }
 
        protected override ValueTask<IChatClient> SelectClientAsync(
            RoutingContext context,
            CancellationToken cancellationToken) =>
            _clientSelector(context, cancellationToken);
    }
}