mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Rename AI Agent packages to use Microsoft.Agents.AI (#913)
* Rename AI Agent packages to use Microsoft.Agents.AI * Fix for build * Fix formatting * Fix formatting * Ignore in VSTHRD200 in migration samples * Ignore in VSTHRD200 in migration samples * Add some missing projects and run format * Fix build errors * Address code review feedback * Fix merge issues --------- Co-authored-by: Mark Wallace <markwallace@microsoft.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
a480ccfd16
commit
32e054f1fe
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>Provides extensions for configuring <see cref="AgentInvokedChatClient"/> instances.</summary>
|
||||
public static class AgentChatClientBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables automatic function call invocation on the chat pipeline.
|
||||
/// </summary>
|
||||
/// <remarks>This works by adding an instance of <see cref="AgentInvokedChatClient"/> with default options.</remarks>
|
||||
/// <param name="builder">The <see cref="ChatClientBuilder"/> being used to build the chat pipeline.</param>
|
||||
/// <returns>The supplied <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
public static ChatClientBuilder UseAgentInvocation(
|
||||
this ChatClientBuilder builder)
|
||||
{
|
||||
_ = Throw.IfNull(builder);
|
||||
|
||||
return builder.Use((innerClient, services) =>
|
||||
new AgentInvokedChatClient(innerClient));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Internal chat client that handle agent invocation details for the chat client pipeline.
|
||||
/// </summary>
|
||||
internal sealed class AgentInvokedChatClient : DelegatingChatClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentInvokedChatClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client to invoke agents.</param>
|
||||
internal AgentInvokedChatClient(IChatClient chatClient)
|
||||
: base(chatClient)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
#pragma warning disable S3358 // Ternary operators should not be nested
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an agent that can be invoked using a chat client.
|
||||
/// </summary>
|
||||
public sealed class ChatClientAgent : AIAgent
|
||||
{
|
||||
private readonly ChatClientAgentOptions? _agentOptions;
|
||||
private readonly AIAgentMetadata _agentMetadata;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Type _chatClientType;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client to use for invoking the agent.</param>
|
||||
/// <param name="instructions">Optional instructions for the agent.</param>
|
||||
/// <param name="name">Optional name for the agent.</param>
|
||||
/// <param name="description">Optional description for the agent.</param>
|
||||
/// <param name="tools">Optional list of tools that the agent can use during invocation.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
|
||||
public ChatClientAgent(IChatClient chatClient, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, ILoggerFactory? loggerFactory = null)
|
||||
: this(
|
||||
chatClient,
|
||||
new ChatClientAgentOptions
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Instructions = instructions,
|
||||
ChatOptions = tools is null ? null : new ChatOptions
|
||||
{
|
||||
Tools = tools,
|
||||
}
|
||||
},
|
||||
loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client to use for invoking the agent.</param>
|
||||
/// <param name="options">Full set of options to configure the agent.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
|
||||
public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
_ = Throw.IfNull(chatClient);
|
||||
|
||||
// Options must be cloned since ChatClientAgentOptions is mutable.
|
||||
this._agentOptions = options?.Clone();
|
||||
|
||||
this._agentMetadata = new AIAgentMetadata(chatClient.GetService<ChatClientMetadata>()?.ProviderName);
|
||||
|
||||
// Get the type of the chat client before wrapping it as an agent invoking chat client.
|
||||
this._chatClientType = chatClient.GetType();
|
||||
|
||||
// If the user has not opted out of using our default decorators, we wrap the chat client.
|
||||
this.ChatClient = options?.UseProvidedChatClientAsIs is true ? chatClient : chatClient.AsAgentInvokedChatClient(options);
|
||||
|
||||
this._logger = (loggerFactory ?? chatClient.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance).CreateLogger<ChatClientAgent>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying chat client used by the agent to invoke chat completions.
|
||||
/// </summary>
|
||||
public IChatClient ChatClient { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Id => this._agentOptions?.Id ?? base.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Name => this._agentOptions?.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Description => this._agentOptions?.Description;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instructions for the agent (optional).
|
||||
/// </summary>
|
||||
public string? Instructions => this._agentOptions?.Instructions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets of the default <see cref="ChatOptions"/> used by the agent.
|
||||
/// </summary>
|
||||
internal ChatOptions? ChatOptions => this._agentOptions?.ChatOptions;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
|
||||
|
||||
(ChatClientAgentThread safeThread, ChatOptions? chatOptions, List<ChatMessage> threadMessages) =
|
||||
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var agentName = this.GetLoggingAgentName();
|
||||
|
||||
this._logger.LogAgentChatClientInvokingAgent(nameof(RunAsync), this.Id, agentName, this._chatClientType);
|
||||
|
||||
// Call the IChatClient and notify the AIContextProvider of any failures.
|
||||
ChatResponse chatResponse;
|
||||
try
|
||||
{
|
||||
chatResponse = await this.ChatClient.GetResponseAsync(threadMessages, chatOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, agentName, this._chatClientType, inputMessages.Count);
|
||||
|
||||
// We can derive the type of supported thread from whether we have a conversation id,
|
||||
// so let's update it and set the conversation id for the service thread case.
|
||||
this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId);
|
||||
|
||||
// Ensure that the author name is set for each message in the response.
|
||||
foreach (ChatMessage chatResponseMessage in chatResponse.Messages)
|
||||
{
|
||||
chatResponseMessage.AuthorName ??= agentName;
|
||||
chatResponseMessage.MessageId ??= Guid.NewGuid().ToString("N");
|
||||
chatResponseMessage.CreatedAt ??= DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
// Only notify the thread of new messages if the chatResponse was successful to avoid inconsistent message state in the thread.
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new(chatResponse) { AgentId = this.Id };
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
|
||||
|
||||
(ChatClientAgentThread safeThread, ChatOptions? chatOptions, List<ChatMessage> threadMessages) =
|
||||
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
int messageCount = threadMessages.Count;
|
||||
var loggingAgentName = this.GetLoggingAgentName();
|
||||
|
||||
this._logger.LogAgentChatClientInvokingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType);
|
||||
|
||||
List<ChatResponseUpdate> responseUpdates = [];
|
||||
|
||||
IAsyncEnumerator<ChatResponseUpdate> responseUpdatesEnumerator;
|
||||
|
||||
try
|
||||
{
|
||||
// Using the enumerator to ensure we consider the case where no updates are returned for notification.
|
||||
responseUpdatesEnumerator = this.ChatClient.GetStreamingResponseAsync(threadMessages, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
this._logger.LogAgentChatClientInvokedStreamingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType);
|
||||
|
||||
bool hasUpdates;
|
||||
try
|
||||
{
|
||||
// Ensure we start the streaming request
|
||||
hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
string? messageId = null;
|
||||
while (hasUpdates)
|
||||
{
|
||||
var update = responseUpdatesEnumerator.Current;
|
||||
if (update is not null)
|
||||
{
|
||||
update.AuthorName ??= this.Name;
|
||||
update.CreatedAt ??= DateTimeOffset.UtcNow;
|
||||
update.MessageId ??= (messageId ??= Guid.NewGuid().ToString("N"));
|
||||
|
||||
responseUpdates.Add(update);
|
||||
yield return new(update) { AgentId = this.Id };
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
var chatResponse = responseUpdates.ToChatResponse();
|
||||
|
||||
// We can derive the type of supported thread from whether we have a conversation id,
|
||||
// so let's update it and set the conversation id for the service thread case.
|
||||
this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId);
|
||||
|
||||
// To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request.
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
base.GetService(serviceType, serviceKey)
|
||||
?? (serviceType == typeof(AIAgentMetadata) ? this._agentMetadata
|
||||
: serviceType == typeof(IChatClient) ? this.ChatClient
|
||||
: this.ChatClient.GetService(serviceType, serviceKey));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread GetNewThread()
|
||||
=> new ChatClientAgentThread
|
||||
{
|
||||
MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }),
|
||||
AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null })
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get a new <see cref="AgentThread"/> instance using an existing conversation id, to continue that conversation.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The conversation id to continue.</param>
|
||||
/// <returns>A new <see cref="AgentThread"/> instance.</returns>
|
||||
/// <remarks>
|
||||
/// Note that any <see cref="AgentThread"/> created with this method will only work with <see cref="ChatClientAgent"/> instances that support storing
|
||||
/// chat history in the underlying service provided by the <see cref="IChatClient"/>.
|
||||
/// </remarks>
|
||||
public AgentThread GetNewThread(string conversationId)
|
||||
=> new ChatClientAgentThread()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null })
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
Func<JsonElement, JsonSerializerOptions?, ChatMessageStore>? chatMessageStoreFactory = this._agentOptions?.ChatMessageStoreFactory is null ?
|
||||
null :
|
||||
(jse, jso) => this._agentOptions.ChatMessageStoreFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso });
|
||||
|
||||
Func<JsonElement, JsonSerializerOptions?, AIContextProvider>? aiContextProviderFactory = this._agentOptions?.AIContextProviderFactory is null ?
|
||||
null :
|
||||
(jse, jso) => this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso });
|
||||
|
||||
return new ChatClientAgentThread(
|
||||
serializedThread,
|
||||
jsonSerializerOptions,
|
||||
chatMessageStoreFactory,
|
||||
aiContextProviderFactory);
|
||||
}
|
||||
|
||||
#region Private
|
||||
|
||||
/// <summary>
|
||||
/// Notify the <see cref="AIContextProvider"/> when an agent run succeeded, if there is an <see cref="AIContextProvider"/>.
|
||||
/// </summary>
|
||||
private static async Task NotifyAIContextProviderOfSuccessAsync(ChatClientAgentThread thread, IEnumerable<ChatMessage> inputMessages, IEnumerable<ChatMessage> responseMessages, CancellationToken cancellationToken)
|
||||
{
|
||||
if (thread.AIContextProvider is not null)
|
||||
{
|
||||
await thread.AIContextProvider.InvokedAsync(new(inputMessages) { ResponseMessages = responseMessages },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify the <see cref="AIContextProvider"/> of any failure during an agent run, if there is an <see cref="AIContextProvider"/>.
|
||||
/// </summary>
|
||||
private static async Task NotifyAIContextProviderOfFailureAsync(ChatClientAgentThread thread, Exception ex, IEnumerable<ChatMessage> inputMessages, CancellationToken cancellationToken)
|
||||
{
|
||||
if (thread.AIContextProvider is not null)
|
||||
{
|
||||
await thread.AIContextProvider.InvokedAsync(new(inputMessages) { InvokeException = ex },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures and returns chat options by merging the provided run options with the agent's default chat options.
|
||||
/// </summary>
|
||||
/// <remarks>This method prioritizes the chat options provided in <paramref name="runOptions"/> over the
|
||||
/// agent's default chat options. Any unset properties in the run options will be filled using the agent's chat
|
||||
/// options. If both are <see langword="null"/>, the method returns <see langword="null"/>.</remarks>
|
||||
/// <param name="runOptions">Optional run options that may include specific chat configuration settings.</param>
|
||||
/// <returns>A <see cref="ChatOptions"/> object representing the merged chat configuration, or <see langword="null"/> if
|
||||
/// neither the run options nor the agent's chat options are available.</returns>
|
||||
private ChatOptions? CreateConfiguredChatOptions(AgentRunOptions? runOptions)
|
||||
{
|
||||
ChatOptions? requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone();
|
||||
|
||||
// If no agent chat options were provided, return the request chat options as is.
|
||||
if (this._agentOptions?.ChatOptions is null)
|
||||
{
|
||||
return requestChatOptions;
|
||||
}
|
||||
|
||||
// If no request chat options were provided, use the agent's chat options clone.
|
||||
if (requestChatOptions is null)
|
||||
{
|
||||
return this._agentOptions?.ChatOptions.Clone();
|
||||
}
|
||||
|
||||
// If both are present, we need to merge them.
|
||||
// The merge strategy will prioritize the request options over the agent options,
|
||||
// and will fill the blanks with agent options where the request options were not set.
|
||||
requestChatOptions.AllowMultipleToolCalls ??= this._agentOptions.ChatOptions.AllowMultipleToolCalls;
|
||||
requestChatOptions.ConversationId ??= this._agentOptions.ChatOptions.ConversationId;
|
||||
requestChatOptions.FrequencyPenalty ??= this._agentOptions.ChatOptions.FrequencyPenalty;
|
||||
requestChatOptions.Instructions ??= this._agentOptions.ChatOptions.Instructions;
|
||||
requestChatOptions.MaxOutputTokens ??= this._agentOptions.ChatOptions.MaxOutputTokens;
|
||||
requestChatOptions.ModelId ??= this._agentOptions.ChatOptions.ModelId;
|
||||
requestChatOptions.PresencePenalty ??= this._agentOptions.ChatOptions.PresencePenalty;
|
||||
requestChatOptions.ResponseFormat ??= this._agentOptions.ChatOptions.ResponseFormat;
|
||||
requestChatOptions.Seed ??= this._agentOptions.ChatOptions.Seed;
|
||||
requestChatOptions.Temperature ??= this._agentOptions.ChatOptions.Temperature;
|
||||
requestChatOptions.TopP ??= this._agentOptions.ChatOptions.TopP;
|
||||
requestChatOptions.TopK ??= this._agentOptions.ChatOptions.TopK;
|
||||
requestChatOptions.ToolMode ??= this._agentOptions.ChatOptions.ToolMode;
|
||||
|
||||
// Merge only the additional properties from the agent if they are not already set in the request options.
|
||||
if (requestChatOptions.AdditionalProperties is not null && this._agentOptions.ChatOptions.AdditionalProperties is not null)
|
||||
{
|
||||
foreach (var propertyKey in this._agentOptions.ChatOptions.AdditionalProperties.Keys)
|
||||
{
|
||||
_ = requestChatOptions.AdditionalProperties.TryAdd(propertyKey, this._agentOptions.ChatOptions.AdditionalProperties[propertyKey]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
requestChatOptions.AdditionalProperties ??= this._agentOptions.ChatOptions.AdditionalProperties?.Clone();
|
||||
}
|
||||
|
||||
// Chain the raw representation factory from the request options with the agent's factory if available.
|
||||
if (this._agentOptions.ChatOptions.RawRepresentationFactory is { } agentFactory)
|
||||
{
|
||||
requestChatOptions.RawRepresentationFactory = requestChatOptions.RawRepresentationFactory is { } requestFactory
|
||||
? chatClient => requestFactory(chatClient) ?? agentFactory(chatClient)
|
||||
: agentFactory;
|
||||
}
|
||||
|
||||
// We concatenate the request stop sequences with the agent's stop sequences when available.
|
||||
if (this._agentOptions.ChatOptions.StopSequences is { Count: not 0 })
|
||||
{
|
||||
if (requestChatOptions.StopSequences is null || requestChatOptions.StopSequences.Count == 0)
|
||||
{
|
||||
// If the request stop sequences are not set or empty, we use the agent's stop sequences directly.
|
||||
requestChatOptions.StopSequences = [.. this._agentOptions.ChatOptions.StopSequences];
|
||||
}
|
||||
else if (requestChatOptions.StopSequences is List<string> requestStopSequences)
|
||||
{
|
||||
// If the request stop sequences are set, we concatenate them with the agent's stop sequences.
|
||||
requestStopSequences.AddRange(this._agentOptions.ChatOptions.StopSequences);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If both agent's and request's stop sequences are set, we concatenate them.
|
||||
foreach (string stopSequence in this._agentOptions.ChatOptions.StopSequences)
|
||||
{
|
||||
requestChatOptions.StopSequences.Add(stopSequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We concatenate the request tools with the agent's tools when available.
|
||||
if (this._agentOptions.ChatOptions.Tools is { Count: not 0 })
|
||||
{
|
||||
if (requestChatOptions.Tools is not { Count: > 0 })
|
||||
{
|
||||
// If the request tools are not set or empty, we use the agent's tools.
|
||||
requestChatOptions.Tools = [.. this._agentOptions.ChatOptions.Tools];
|
||||
}
|
||||
else
|
||||
{
|
||||
if (requestChatOptions.Tools is List<AITool> requestTools)
|
||||
{
|
||||
// If the request tools are set, we concatenate them with the agent's tools.
|
||||
requestTools.AddRange(this._agentOptions.ChatOptions.Tools);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the both agent's and request's tools are set, we concatenate all tools.
|
||||
foreach (var tool in this._agentOptions.ChatOptions.Tools)
|
||||
{
|
||||
requestChatOptions.Tools.Add(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return requestChatOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the thread, chat options, and messages for agent execution.
|
||||
/// </summary>
|
||||
/// <param name="thread">The conversation thread to use or create.</param>
|
||||
/// <param name="inputMessages">The input messages to use.</param>
|
||||
/// <param name="runOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A tuple containing the thread, chat options, and thread messages.</returns>
|
||||
private async Task<(ChatClientAgentThread AgentThread, ChatOptions? ChatOptions, List<ChatMessage> ThreadMessages)> PrepareThreadAndMessagesAsync(
|
||||
AgentThread? thread,
|
||||
IEnumerable<ChatMessage> inputMessages,
|
||||
AgentRunOptions? runOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ChatOptions? chatOptions = this.CreateConfiguredChatOptions(runOptions);
|
||||
|
||||
thread ??= this.GetNewThread();
|
||||
if (thread is not ChatClientAgentThread typedThread)
|
||||
{
|
||||
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
|
||||
}
|
||||
|
||||
// Add any existing messages from the thread to the messages to be sent to the chat client.
|
||||
List<ChatMessage> threadMessages = [];
|
||||
if (typedThread.MessageStore is not null)
|
||||
{
|
||||
threadMessages.AddRange(await typedThread.MessageStore.GetMessagesAsync(cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
// If we have an AIContextProvider, we should get context from it, and update our
|
||||
// messages and options with the additional context.
|
||||
if (typedThread.AIContextProvider is not null)
|
||||
{
|
||||
var invokingContext = new AIContextProvider.InvokingContext(inputMessages);
|
||||
var aiContext = await typedThread.AIContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
|
||||
if (aiContext.Messages is { Count: > 0 })
|
||||
{
|
||||
threadMessages.AddRange(aiContext.Messages);
|
||||
}
|
||||
|
||||
if (aiContext.Tools is { Count: > 0 })
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Tools ??= [];
|
||||
foreach (AITool tool in aiContext.Tools)
|
||||
{
|
||||
chatOptions.Tools.Add(tool);
|
||||
}
|
||||
}
|
||||
|
||||
if (aiContext.Instructions is not null)
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? aiContext.Instructions : $"{chatOptions.Instructions}\n{aiContext.Instructions}";
|
||||
}
|
||||
}
|
||||
|
||||
// Add the input messages to the end of thread messages.
|
||||
threadMessages.AddRange(inputMessages);
|
||||
|
||||
// If a user provided two different thread ids, via the thread object and options, we should throw
|
||||
// since we don't know which one to use.
|
||||
if (!string.IsNullOrWhiteSpace(typedThread.ConversationId) && !string.IsNullOrWhiteSpace(chatOptions?.ConversationId) && typedThread.ConversationId != chatOptions!.ConversationId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"""
|
||||
The {nameof(chatOptions.ConversationId)} provided via {nameof(Extensions.AI.ChatOptions)} is different to the id of the provided {nameof(AgentThread)}.
|
||||
Only one id can be used for a run.
|
||||
""");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(this.Instructions))
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? this.Instructions : $"{this.Instructions}\n{chatOptions.Instructions}";
|
||||
}
|
||||
|
||||
// Only create or update ChatOptions if we have an id on the thread and we don't have the same one already in ChatOptions.
|
||||
if (!string.IsNullOrWhiteSpace(typedThread.ConversationId) && typedThread.ConversationId != chatOptions?.ConversationId)
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.ConversationId = typedThread.ConversationId;
|
||||
}
|
||||
|
||||
return (typedThread, chatOptions, threadMessages);
|
||||
}
|
||||
|
||||
private void UpdateThreadWithTypeAndConversationId(ChatClientAgentThread thread, string? responseConversationId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(responseConversationId) && !string.IsNullOrWhiteSpace(thread.ConversationId))
|
||||
{
|
||||
// We were passed a thread that is service managed, but we got no conversation id back from the chat client,
|
||||
// meaning the service doesn't support service managed threads, so the thread cannot be used with this service.
|
||||
throw new InvalidOperationException("Service did not return a valid conversation id when using a service managed thread.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(responseConversationId))
|
||||
{
|
||||
// If we got a conversation id back from the chat client, it means that the service supports server side thread storage
|
||||
// so we should update the thread with the new id.
|
||||
thread.ConversationId = responseConversationId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the service doesn't use service side thread storage (i.e. we got no id back from invocation), and
|
||||
// the thread has no MessageStore yet, and we have a custom messages store, we should update the thread
|
||||
// with the custom MessageStore so that it has somewhere to store the chat history.
|
||||
thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null });
|
||||
}
|
||||
}
|
||||
|
||||
private string GetLoggingAgentName() => this.Name ?? "UnnamedAgent";
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
#pragma warning disable SYSLIB1006 // Multiple logging methods cannot use the same event id within a class
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for logging <see cref="ChatClientAgent"/> invocations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
|
||||
/// generate logging code at compile time to achieve optimized code.
|
||||
/// </remarks>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class ChatClientAgentLogMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs <see cref="ChatClientAgent"/> invoking agent (started).
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoking client {ClientType}.")]
|
||||
public static partial void LogAgentChatClientInvokingAgent(
|
||||
this ILogger logger,
|
||||
string methodName,
|
||||
string agentId,
|
||||
string agentName,
|
||||
Type clientType);
|
||||
|
||||
/// <summary>
|
||||
/// Logs <see cref="ChatClientAgent"/> invoked agent (complete).
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoked client {ClientType} with message count: {MessageCount}.")]
|
||||
public static partial void LogAgentChatClientInvokedAgent(
|
||||
this ILogger logger,
|
||||
string methodName,
|
||||
string agentId,
|
||||
string agentName,
|
||||
Type clientType,
|
||||
int messageCount);
|
||||
|
||||
/// <summary>
|
||||
/// Logs <see cref="ChatClientAgent"/> invoked streaming agent (complete).
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoked client {ClientType}.")]
|
||||
public static partial void LogAgentChatClientInvokedStreamingAgent(
|
||||
this ILogger logger,
|
||||
string methodName,
|
||||
string agentId,
|
||||
string agentName,
|
||||
Type clientType);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents metadata for a chat client agent, including its identifier, name, instructions, and description.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is used to encapsulate information about a chat client agent, such as its unique
|
||||
/// identifier, display name, operational instructions, and a descriptive summary. It can be used to store and transfer
|
||||
/// agent-related metadata within a chat application.
|
||||
/// </remarks>
|
||||
public class ChatClientAgentOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentOptions"/> class.
|
||||
/// </summary>
|
||||
public ChatClientAgentOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentOptions"/> class with the specified parameters.
|
||||
/// </summary>
|
||||
/// <remarks>If <paramref name="tools"/> is provided, a new <see cref="ChatOptions"/> instance is created
|
||||
/// with the specified instructions and tools.</remarks>
|
||||
/// <param name="instructions">The instructions or guidelines for the chat client agent. Can be <see langword="null"/> if not specified.</param>
|
||||
/// <param name="name">The name of the chat client agent. Can be <see langword="null"/> if not specified.</param>
|
||||
/// <param name="description">The description of the chat client agent. Can be <see langword="null"/> if not specified.</param>
|
||||
/// <param name="tools">A list of <see cref="AITool"/> instances available to the chat client agent. Can be <see langword="null"/> if no
|
||||
/// tools are specified.</param>
|
||||
public ChatClientAgentOptions(string? instructions, string? name = null, string? description = null, IList<AITool>? tools = null)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Instructions = instructions;
|
||||
this.Description = description;
|
||||
|
||||
if (tools is not null)
|
||||
{
|
||||
(this.ChatOptions ??= new()).Tools = tools;
|
||||
}
|
||||
|
||||
if (instructions is not null)
|
||||
{
|
||||
(this.ChatOptions ??= new()).Instructions = instructions;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the agent id.
|
||||
/// </summary>
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the agent name.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the agent instructions.
|
||||
/// </summary>
|
||||
public string? Instructions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the agent description.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default chatOptions to use.
|
||||
/// </summary>
|
||||
public ChatOptions? ChatOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a factory function to create an instance of <see cref="ChatMessageStore"/>
|
||||
/// which will be used to store chat messages for this agent.
|
||||
/// </summary>
|
||||
public Func<ChatMessageStoreFactoryContext, ChatMessageStore>? ChatMessageStoreFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a factory function to create an instance of <see cref="AIContextProvider"/>
|
||||
/// which will be used to create a context provider for each new thread, and can then
|
||||
/// provide additional context for each agent run.
|
||||
/// </summary>
|
||||
public Func<AIContextProviderFactoryContext, AIContextProvider>? AIContextProviderFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use the provided <see cref="IChatClient"/> instance as is,
|
||||
/// without applying any default decorators.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default the <see cref="ChatClientAgent"/> applies decorators to the provided <see cref="IChatClient"/>
|
||||
/// for doing for example automatic function invocation. Setting this property to <see langword="true"/>
|
||||
/// disables adding these default decorators.
|
||||
/// Disabling is recommended if you want to decorate the <see cref="IChatClient"/> with different decorators
|
||||
/// than the default ones. The provided <see cref="IChatClient"/> instance should then already be decorated
|
||||
/// with the desired decorators.
|
||||
/// </remarks>
|
||||
public bool UseProvidedChatClientAsIs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance.
|
||||
/// </summary>
|
||||
internal ChatClientAgentOptions Clone()
|
||||
=> new()
|
||||
{
|
||||
Id = this.Id,
|
||||
Name = this.Name,
|
||||
Instructions = this.Instructions,
|
||||
Description = this.Description,
|
||||
ChatOptions = this.ChatOptions?.Clone(),
|
||||
ChatMessageStoreFactory = this.ChatMessageStoreFactory,
|
||||
AIContextProviderFactory = this.AIContextProviderFactory,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Context object passed to the <see cref="AIContextProviderFactory"/> to create a new instance of <see cref="AIContextProvider"/>.
|
||||
/// </summary>
|
||||
public class AIContextProviderFactoryContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the serialized state of the <see cref="AIContextProvider"/>, if any.
|
||||
/// </summary>
|
||||
/// <value><see langword="default"/> if there is no state, e.g. when the <see cref="AIContextProvider"/> is first created.</value>
|
||||
public JsonElement SerializedState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the JSON serialization options to use when deserializing the <see cref="SerializedState"/>.
|
||||
/// </summary>
|
||||
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context object passed to the <see cref="ChatMessageStoreFactory"/> to create a new instance of <see cref="ChatMessageStore"/>.
|
||||
/// </summary>
|
||||
public class ChatMessageStoreFactoryContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the serialized state of the chat message store, if any.
|
||||
/// </summary>
|
||||
/// <value><see langword="default"/> if there is no state, e.g. when the <see cref="ChatMessageStore"/> is first created.</value>
|
||||
public JsonElement SerializedState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the JSON serialization options to use when deserializing the <see cref="SerializedState"/>.
|
||||
/// </summary>
|
||||
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Chat client agent run options.
|
||||
/// </summary>
|
||||
public sealed class ChatClientAgentRunOptions : AgentRunOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentRunOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatOptions">Optional chat options to pass to the agent's invocation.</param>
|
||||
public ChatClientAgentRunOptions(ChatOptions? chatOptions = null)
|
||||
{
|
||||
this.ChatOptions = chatOptions;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets optional chat options to pass to the agent's invocation.</summary>
|
||||
public ChatOptions? ChatOptions { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Thread for ChatClient based agents.
|
||||
/// </summary>
|
||||
public class ChatClientAgentThread : AgentThread
|
||||
{
|
||||
private string? _conversationId;
|
||||
private ChatMessageStore? _messageStore;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentThread"/> class.
|
||||
/// </summary>
|
||||
internal ChatClientAgentThread()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentThread"/> class from serialized state.
|
||||
/// </summary>
|
||||
/// <param name="serializedThreadState">A <see cref="JsonElement"/> representing the serialized state of the thread.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
|
||||
/// <param name="chatMessageStoreFactory">An optional factory function to create a custom <see cref="ChatMessageStore"/>.</param>
|
||||
/// <param name="aiContextProviderFactory">An optional factory function to create a custom <see cref="AIContextProvider"/>.</param>
|
||||
internal ChatClientAgentThread(
|
||||
JsonElement serializedThreadState,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
Func<JsonElement, JsonSerializerOptions?, ChatMessageStore>? chatMessageStoreFactory = null,
|
||||
Func<JsonElement, JsonSerializerOptions?, AIContextProvider>? aiContextProviderFactory = null)
|
||||
{
|
||||
if (serializedThreadState.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new ArgumentException("The serialized thread state must be a JSON object.", nameof(serializedThreadState));
|
||||
}
|
||||
|
||||
var state = serializedThreadState.Deserialize(
|
||||
AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState))) as ThreadState;
|
||||
|
||||
this.AIContextProvider = aiContextProviderFactory?.Invoke(state?.AIContextProviderState ?? default, jsonSerializerOptions);
|
||||
|
||||
if (state?.ConversationId is string threadId)
|
||||
{
|
||||
this.ConversationId = threadId;
|
||||
|
||||
// Since we have an ID, we should not have a chat message store and we can return here.
|
||||
return;
|
||||
}
|
||||
|
||||
this._messageStore =
|
||||
chatMessageStoreFactory?.Invoke(state?.StoreState ?? default, jsonSerializerOptions) ??
|
||||
new InMemoryChatMessageStore(state?.StoreState ?? default, jsonSerializerOptions); // default to an in-memory store
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the underlying service thread to support cases where the chat history is stored by the agent service.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Note that either <see cref="ConversationId"/> or <see cref="MessageStore "/> may be set, but not both.
|
||||
/// If <see cref="MessageStore "/> is not null, and <see cref="ConversationId"/> is set, <see cref="MessageStore "/>
|
||||
/// will be reverted to null, and vice versa.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property may be null in the following cases:
|
||||
/// <list type="bullet">
|
||||
/// <item>The thread stores messages via the <see cref="ChatMessageStore"/> and not in the agent service.</item>
|
||||
/// <item>This thread object is new and a server managed thread has not yet been created in the agent service.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The id may also change over time where the id is pointing at a
|
||||
/// agent service managed thread, and the default behavior of a service is
|
||||
/// to fork the thread with each iteration.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public string? ConversationId
|
||||
{
|
||||
get => this._conversationId;
|
||||
internal set
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this._conversationId) && string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._messageStore is not null)
|
||||
{
|
||||
// If we have a message store already, we shouldn't switch the thread to use a conversation id
|
||||
// since it means that the thread contents will essentially be deleted, and the thread will not work
|
||||
// with the original agent anymore.
|
||||
throw new InvalidOperationException("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.");
|
||||
}
|
||||
|
||||
this._conversationId = Throw.IfNullOrWhitespace(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ChatMessageStore"/> used by this thread, for cases where messages should be stored in a custom location.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Note that either <see cref="ConversationId"/> or <see cref="MessageStore "/> may be set, but not both.
|
||||
/// If <see cref="ConversationId"/> is not null, and <see cref="MessageStore "/> is set, <see cref="ConversationId"/>
|
||||
/// will be reverted to null, and vice versa.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property may be null in the following cases:
|
||||
/// <list type="bullet">
|
||||
/// <item>The thread stores messages in the agent service and just has an id to the remove thread, instead of in an <see cref="ChatMessageStore"/>.</item>
|
||||
/// <item>This thread object is new it is not yet clear whether it will be backed by a server managed thread or an <see cref="ChatMessageStore"/>.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ChatMessageStore? MessageStore
|
||||
{
|
||||
get => this._messageStore;
|
||||
internal set
|
||||
{
|
||||
if (this._messageStore is null && value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(this._conversationId))
|
||||
{
|
||||
// If we have a conversation id already, we shouldn't switch the thread to use a message store
|
||||
// since it means that the thread will not work with the original agent anymore.
|
||||
throw new InvalidOperationException("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.");
|
||||
}
|
||||
|
||||
this._messageStore = Throw.IfNull(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="AIContextProvider"/> used by this thread to provide additional context to the AI model before each invocation.
|
||||
/// </summary>
|
||||
public AIContextProvider? AIContextProvider { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
|
||||
/// </summary>
|
||||
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
|
||||
public override async Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var storeState = this._messageStore is null ?
|
||||
null :
|
||||
await this._messageStore.SerializeStateAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var aiContextProviderState = this.AIContextProvider is null ?
|
||||
null :
|
||||
await this.AIContextProvider.SerializeAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var state = new ThreadState
|
||||
{
|
||||
ConversationId = this.ConversationId,
|
||||
StoreState = storeState,
|
||||
AIContextProviderState = aiContextProviderState
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
serviceType == typeof(AgentThreadMetadata)
|
||||
? new AgentThreadMetadata(this.ConversationId)
|
||||
: base.GetService(serviceType, serviceKey)
|
||||
?? this.AIContextProvider?.GetService(serviceType, serviceKey)
|
||||
?? this.MessageStore?.GetService(serviceType, serviceKey);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
|
||||
{
|
||||
switch (this)
|
||||
{
|
||||
case { ConversationId: not null }:
|
||||
// If the thread messages are stored in the service
|
||||
// there is nothing to do here, since invoking the
|
||||
// service should already update the thread.
|
||||
break;
|
||||
|
||||
case { MessageStore: null }:
|
||||
// If there is no conversation id, and no store we can createa a default in memory store and add messages to it.
|
||||
this._messageStore = new InMemoryChatMessageStore();
|
||||
await this._messageStore!.AddMessagesAsync(newMessages, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case { MessageStore: not null }:
|
||||
// If a store has been provided, we need to add the messages to the store.
|
||||
await this._messageStore!.AddMessagesAsync(newMessages, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ThreadState
|
||||
{
|
||||
public string? ConversationId { get; set; }
|
||||
|
||||
public JsonElement? StoreState { get; set; }
|
||||
|
||||
public JsonElement? AIContextProviderState { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Extensions.AI;
|
||||
|
||||
internal static class ChatClientExtensions
|
||||
{
|
||||
internal static IChatClient AsAgentInvokedChatClient(this IChatClient chatClient, ChatClientAgentOptions? options)
|
||||
{
|
||||
var chatBuilder = chatClient.AsBuilder();
|
||||
|
||||
// AgentInvokingChatClient should be the outermost decorator
|
||||
if (chatClient is not AgentInvokedChatClient agentInvokingChatClient)
|
||||
{
|
||||
chatBuilder.UseAgentInvocation();
|
||||
}
|
||||
|
||||
if (chatClient.GetService<FunctionInvokingChatClient>() is null)
|
||||
{
|
||||
_ = chatBuilder.Use((innerClient, services) =>
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
|
||||
return new FunctionInvokingChatClient(innerClient, loggerFactory, services);
|
||||
});
|
||||
}
|
||||
|
||||
var agentChatClient = chatBuilder.Build();
|
||||
|
||||
if (options?.ChatOptions?.Tools is { Count: > 0 })
|
||||
{
|
||||
// When tools are provided in the constructor, set the tools for the whole lifecycle of the chat client
|
||||
var functionService = agentChatClient.GetService<FunctionInvokingChatClient>();
|
||||
Debug.Assert(functionService is not null, "FunctionInvokingChatClient should be registered in the chat client.");
|
||||
functionService!.AdditionalTools = options.ChatOptions.Tools;
|
||||
}
|
||||
|
||||
return agentChatClient;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user