mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
a3a9147e61
* Rename AgentThread to AgentSession * Add more renames * Update readme files * Revert nullable variable change and further fixes. * Revert change in header name * Fix some comments and tests * Update changelog. * Address PR feedback. * Fixing code review comments. * Fix new errors after merging latest code.
204 lines
9.7 KiB
C#
204 lines
9.7 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Threading.Channels;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.AI;
|
|
using Microsoft.Shared.Diagnostics;
|
|
|
|
namespace Microsoft.Agents.AI;
|
|
|
|
/// <summary>Represents a delegating AI agent that wraps an inner agent with implementations provided by delegates.</summary>
|
|
/// <remarks>
|
|
/// This internal class is a convenience implementation mainly used to support <see cref="AIAgentBuilder"/> Use methods that take delegates to intercept agent operations.
|
|
/// </remarks>
|
|
internal sealed class AnonymousDelegatingAIAgent : DelegatingAIAgent
|
|
{
|
|
/// <summary>The delegate to use as the implementation of <see cref="RunCoreAsync"/>.</summary>
|
|
private readonly Func<IEnumerable<ChatMessage>, AgentSession?, AgentRunOptions?, AIAgent, CancellationToken, Task<AgentResponse>>? _runFunc;
|
|
|
|
/// <summary>The delegate to use as the implementation of <see cref="RunCoreStreamingAsync"/>.</summary>
|
|
/// <remarks>
|
|
/// When non-<see langword="null"/>, this delegate is used as the implementation of <see cref="RunCoreStreamingAsync"/> and
|
|
/// will be invoked with the same arguments as the method itself.
|
|
/// When <see langword="null"/>, <see cref="RunCoreStreamingAsync"/> will delegate directly to the inner agent.
|
|
/// </remarks>
|
|
private readonly Func<IEnumerable<ChatMessage>, AgentSession?, AgentRunOptions?, AIAgent, CancellationToken, IAsyncEnumerable<AgentResponseUpdate>>? _runStreamingFunc;
|
|
|
|
/// <summary>The delegate to use as the implementation of both <see cref="RunCoreAsync"/> and <see cref="RunCoreStreamingAsync"/>.</summary>
|
|
private readonly Func<IEnumerable<ChatMessage>, AgentSession?, AgentRunOptions?, Func<IEnumerable<ChatMessage>, AgentSession?, AgentRunOptions?, CancellationToken, Task>, CancellationToken, Task>? _sharedFunc;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AnonymousDelegatingAIAgent"/> class.
|
|
/// </summary>
|
|
/// <param name="innerAgent">The inner agent.</param>
|
|
/// <param name="sharedFunc">
|
|
/// A delegate that provides the implementation for both <see cref="RunCoreAsync"/> and <see cref="RunCoreStreamingAsync"/>.
|
|
/// In addition to the arguments for the operation, it's provided with a delegate to the inner agent that should be
|
|
/// used to perform the operation on the inner agent. It will handle both the non-streaming and streaming cases.
|
|
/// </param>
|
|
/// <remarks>
|
|
/// This overload may be used when the anonymous implementation needs to provide pre-processing and/or post-processing, but doesn't
|
|
/// need to interact with the results of the operation, which will come from the inner agent.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentNullException"><paramref name="innerAgent"/> is <see langword="null"/>.</exception>
|
|
/// <exception cref="ArgumentNullException"><paramref name="sharedFunc"/> is <see langword="null"/>.</exception>
|
|
public AnonymousDelegatingAIAgent(
|
|
AIAgent innerAgent,
|
|
Func<IEnumerable<ChatMessage>, AgentSession?, AgentRunOptions?, Func<IEnumerable<ChatMessage>, AgentSession?, AgentRunOptions?, CancellationToken, Task>, CancellationToken, Task> sharedFunc)
|
|
: base(innerAgent)
|
|
{
|
|
_ = Throw.IfNull(sharedFunc);
|
|
|
|
this._sharedFunc = sharedFunc;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AnonymousDelegatingAIAgent"/> class.
|
|
/// </summary>
|
|
/// <param name="innerAgent">The inner agent.</param>
|
|
/// <param name="runFunc">
|
|
/// A delegate that provides the implementation for <see cref="RunCoreAsync"/>. When <see langword="null"/>,
|
|
/// <paramref name="runStreamingFunc"/> must be non-null, and the implementation of <see cref="RunCoreAsync"/>
|
|
/// will use <paramref name="runStreamingFunc"/> for the implementation.
|
|
/// </param>
|
|
/// <param name="runStreamingFunc">
|
|
/// A delegate that provides the implementation for <see cref="RunCoreStreamingAsync"/>. When <see langword="null"/>,
|
|
/// <paramref name="runFunc"/> must be non-null, and the implementation of <see cref="RunCoreStreamingAsync"/>
|
|
/// will use <paramref name="runFunc"/> for the implementation.
|
|
/// </param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="innerAgent"/> is <see langword="null"/>.</exception>
|
|
/// <exception cref="ArgumentNullException">Both <paramref name="runFunc"/> and <paramref name="runStreamingFunc"/> are <see langword="null"/>.</exception>
|
|
public AnonymousDelegatingAIAgent(
|
|
AIAgent innerAgent,
|
|
Func<IEnumerable<ChatMessage>, AgentSession?, AgentRunOptions?, AIAgent, CancellationToken, Task<AgentResponse>>? runFunc,
|
|
Func<IEnumerable<ChatMessage>, AgentSession?, AgentRunOptions?, AIAgent, CancellationToken, IAsyncEnumerable<AgentResponseUpdate>>? runStreamingFunc)
|
|
: base(innerAgent)
|
|
{
|
|
ThrowIfBothDelegatesNull(runFunc, runStreamingFunc);
|
|
|
|
this._runFunc = runFunc;
|
|
this._runStreamingFunc = runStreamingFunc;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override Task<AgentResponse> RunCoreAsync(
|
|
IEnumerable<ChatMessage> messages,
|
|
AgentSession? session = null,
|
|
AgentRunOptions? options = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
_ = Throw.IfNull(messages);
|
|
|
|
if (this._sharedFunc is not null)
|
|
{
|
|
return GetRunViaSharedAsync(messages, session, options, cancellationToken);
|
|
|
|
async Task<AgentResponse> GetRunViaSharedAsync(
|
|
IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, CancellationToken cancellationToken)
|
|
{
|
|
AgentResponse? response = null;
|
|
|
|
await this._sharedFunc(
|
|
messages,
|
|
session,
|
|
options,
|
|
async (messages, session, options, cancellationToken)
|
|
=> response = await this.InnerAgent.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false),
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (response is null)
|
|
{
|
|
Throw.InvalidOperationException("The shared delegate completed successfully without producing an AgentResponse.");
|
|
}
|
|
|
|
return response;
|
|
}
|
|
}
|
|
else if (this._runFunc is not null)
|
|
{
|
|
return this._runFunc(messages, session, options, this.InnerAgent, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(this._runStreamingFunc is not null, "Expected non-null streaming delegate.");
|
|
return this._runStreamingFunc!(messages, session, options, this.InnerAgent, cancellationToken)
|
|
.ToAgentResponseAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
|
IEnumerable<ChatMessage> messages,
|
|
AgentSession? session = null,
|
|
AgentRunOptions? options = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
_ = Throw.IfNull(messages);
|
|
|
|
if (this._sharedFunc is not null)
|
|
{
|
|
var updates = Channel.CreateBounded<AgentResponseUpdate>(1);
|
|
|
|
_ = ProcessAsync();
|
|
async Task ProcessAsync()
|
|
{
|
|
Exception? error = null;
|
|
try
|
|
{
|
|
await this._sharedFunc(messages, session, options, async (messages, session, options, cancellationToken) =>
|
|
{
|
|
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
|
{
|
|
await updates.Writer.WriteAsync(update, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex;
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_ = updates.Writer.TryComplete(error);
|
|
}
|
|
}
|
|
|
|
return updates.Reader.ReadAllAsync(cancellationToken);
|
|
}
|
|
else if (this._runStreamingFunc is not null)
|
|
{
|
|
return this._runStreamingFunc(messages, session, options, this.InnerAgent, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(this._runFunc is not null, "Expected non-null non-streaming delegate.");
|
|
return GetStreamingRunAsyncViaRunAsync(this._runFunc!(messages, session, options, this.InnerAgent, cancellationToken));
|
|
|
|
static async IAsyncEnumerable<AgentResponseUpdate> GetStreamingRunAsyncViaRunAsync(Task<AgentResponse> task)
|
|
{
|
|
AgentResponse response = await task.ConfigureAwait(false);
|
|
foreach (var update in response.ToAgentResponseUpdates())
|
|
{
|
|
yield return update;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Throws an exception if both of the specified delegates are <see langword="null"/>.</summary>
|
|
/// <exception cref="ArgumentNullException">Both <paramref name="runFunc"/> and <paramref name="runStreamingFunc"/> are <see langword="null"/>.</exception>
|
|
internal static void ThrowIfBothDelegatesNull(object? runFunc, object? runStreamingFunc)
|
|
{
|
|
if (runFunc is null && runStreamingFunc is null)
|
|
{
|
|
Throw.ArgumentNullException(nameof(runFunc), $"At least one of the {nameof(runFunc)} or {nameof(runStreamingFunc)} delegates must be non-null.");
|
|
}
|
|
}
|
|
}
|