Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 80 additions & 33 deletions dotnet/src/Microsoft.Agents/ChatCompletion/ChatClientAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
Expand All @@ -19,6 +20,7 @@ public sealed class ChatClientAgent : Agent
{
private readonly ChatClientAgentOptions? _agentOptions;
private readonly ILogger _logger;
private readonly Type _chatClientType;

/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAgent"/> class.
Expand All @@ -30,26 +32,24 @@ public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options =
{
Throw.IfNull(chatClient);

this._chatClientType = chatClient.GetType();
this.ChatClient = chatClient.AsAgentInvokingChatClient();
this._agentOptions = options;
this._logger = (loggerFactory ?? chatClient.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance).CreateLogger<ChatClientAgent>();
}

/// <summary>
/// The chat client.
/// The underlying chat client used by the agent to invoke chat completions.
/// </summary>
public IChatClient ChatClient { get; }

/// <summary>
/// Gets the role used for agent instructions. Defaults to "system".
/// Gets the role used for agent instruction messages. Defaults to <see cref="ChatRole.System"/>.
/// </summary>
/// <remarks>
/// Certain versions of "O*" series (deep reasoning) models require the instructions
/// to be provided as "developer" role. Other versions support neither role and
/// an agent targeting such a model cannot provide instructions. Agent functionality
/// will be dictated entirely by the provided plugins.
/// Depending on the AI model used, some APIs may require the instructions message role to be different from <see cref="ChatRole.System"/>.
/// </remarks>
public ChatRole InstructionsRole { get; set; } = ChatRole.System;
public ChatRole InstructionsRole => this._agentOptions?.InstructionsRole ?? ChatRole.System;

/// <inheritdoc/>
public override string Id => this._agentOptions?.Id ?? base.Id;
Expand All @@ -72,35 +72,16 @@ public override async Task<ChatResponse> RunAsync(
{
Throw.IfNull(messages);

// Retrieve chat options from the provided AgentRunOptions if available.
ChatOptions? chatOptions = (options as ChatClientAgentRunOptions)?.ChatOptions;

var chatClientThread = this.ValidateOrCreateThreadType<ChatClientAgentThread>(thread, () => new());

// Add any existing messages from the thread to the messages to be sent to the chat client.
List<ChatMessage> threadMessages = [];
if (chatClientThread is IMessagesRetrievableThread messagesRetrievableThread)
{
await foreach (ChatMessage message in messagesRetrievableThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false))
{
threadMessages.Add(message);
}
}
(ChatClientAgentThread chatClientThread, ChatOptions? chatOptions, List<ChatMessage> threadMessages) =
await this.PrepareThreadAndMessagesAsync(thread, messages, options, cancellationToken).ConfigureAwait(false);

// Append to the existing thread messages the messages that were passed in to this call.
threadMessages.AddRange(messages);
var agentName = this.GetAgentName();

// Update the messages with agent instructions.
this.UpdateThreadMessagesWithAgentInstructions(threadMessages, options);

var agentName = this.Name ?? "UnnamedAgent";
Type serviceType = this.ChatClient.GetType();

this._logger.LogAgentChatClientInvokingAgent(nameof(RunAsync), this.Id, agentName, serviceType);
this._logger.LogAgentChatClientInvokingAgent(nameof(RunAsync), this.Id, agentName, this._chatClientType);

ChatResponse chatResponse = await this.ChatClient.GetResponseAsync(threadMessages, chatOptions, cancellationToken).ConfigureAwait(false);

this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, agentName, serviceType, messages.Count);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, agentName, this._chatClientType, messages.Count);

// Only notify the thread of new messages if the chatResponse was successful to avoid inconsistent messages state in the thread.
await this.NotifyThreadOfNewMessagesAsync(chatClientThread, messages, cancellationToken).ConfigureAwait(false);
Expand All @@ -124,16 +105,81 @@ public override async Task<ChatResponse> RunAsync(
}

/// <inheritdoc/>
public override IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
public override async IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
Throw.IfNull(messages);

(ChatClientAgentThread chatClientThread, ChatOptions? chatOptions, List<ChatMessage> threadMessages) =
await this.PrepareThreadAndMessagesAsync(thread, messages, options, cancellationToken).ConfigureAwait(false);

int messageCount = threadMessages.Count;
var agentName = this.GetAgentName();

this._logger.LogAgentChatClientInvokingAgent(nameof(RunStreamingAsync), this.Id, agentName, this._chatClientType);

var responseUpdatesEnumerable = this.ChatClient.GetStreamingResponseAsync(threadMessages, chatOptions, cancellationToken);

this._logger.LogAgentChatClientInvokedStreamingAgent(nameof(RunStreamingAsync), this.Id, agentName, this._chatClientType);

List<ChatResponseUpdate> responseUpdates = [];
await foreach (ChatResponseUpdate update in responseUpdatesEnumerable.ConfigureAwait(false))
{
responseUpdates.Add(update);
update.AuthorName ??= agentName;
yield return update;
}

var chatResponse = responseUpdates.ToChatResponse();
await this.NotifyThreadOfNewMessagesAsync(chatClientThread, [.. chatResponse.Messages], cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public override AgentThread GetNewThread() => new ChatClientAgentThread();

#region Private

/// <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="options">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 thread, ChatOptions? chatOptions, List<ChatMessage> threadMessages)> PrepareThreadAndMessagesAsync(
AgentThread? thread,
IReadOnlyCollection<ChatMessage> inputMessages,
AgentRunOptions? options,
CancellationToken cancellationToken)
{
// Retrieve chat options from the provided AgentRunOptions if available.
ChatOptions? chatOptions = (options as ChatClientAgentRunOptions)?.ChatOptions;

var chatClientThread = this.ValidateOrCreateThreadType<ChatClientAgentThread>(thread, () => new());

// Add any existing messages from the thread to the messages to be sent to the chat client.
List<ChatMessage> threadMessages = [];
if (chatClientThread is IMessagesRetrievableThread messagesRetrievableThread)
{
await foreach (ChatMessage message in messagesRetrievableThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false))
{
threadMessages.Add(message);
}
}

// Update the messages with agent instructions.
this.UpdateThreadMessagesWithAgentInstructions(threadMessages, options);

// Add the input messages to the end of thread messages.
threadMessages.AddRange(inputMessages);

return (chatClientThread, chatOptions, threadMessages);
}

private void UpdateThreadMessagesWithAgentInstructions(List<ChatMessage> threadMessages, AgentRunOptions? options)
{
if (!string.IsNullOrWhiteSpace(options?.AdditionalInstructions))
Expand All @@ -147,5 +193,6 @@ private void UpdateThreadMessagesWithAgentInstructions(List<ChatMessage> threadM
}
}

private string GetAgentName() => this.Name ?? "UnnamedAgent";
#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,49 @@ namespace Microsoft.Agents;
public static class ChatClientAgentExtensions
{
/// <summary>
/// Allow running a chat client agent with a <see cref="ChatOptions"/> configuration.
/// Run the agent with the provided message and arguments.
/// </summary>
/// <param name="agent">Target agent to run.</param>
/// <param name="messages">Messages to send to the agent.</param>
/// <param name="thread">Optional thread to use for the agent.</param>
/// <param name="agentOptions">Optional agent run options.</param>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
/// <param name="chatOptions">Optional chat options.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A task representing the asynchronous operation, with the chat response.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public static Task<ChatResponse> RunAsync(
this ChatClientAgent agent,
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? agentOptions = null,
AgentRunOptions? agentRunOptions = null,
ChatOptions? chatOptions = null,
CancellationToken cancellationToken = default)
{
Throw.IfNull(agent);
Throw.IfNull(messages);

return agent.RunAsync(messages, thread, new ChatClientAgentRunOptions(agentOptions, chatOptions), cancellationToken);
return agent.RunAsync(messages, thread, new ChatClientAgentRunOptions(agentRunOptions, chatOptions), cancellationToken);
}

/// <summary>
/// Run the agent with the provided message and arguments.
/// </summary>
/// <param name="agent">Target agent to run.</param>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
/// <param name="chatOptions">Optional chat options.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
public static IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
this ChatClientAgent agent,
IReadOnlyCollection<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? agentRunOptions = null,
ChatOptions? chatOptions = null,
CancellationToken cancellationToken = default)
{
Throw.IfNull(agent);
Throw.IfNull(messages);

return agent.RunStreamingAsync(messages, thread, new ChatClientAgentRunOptions(agentRunOptions, chatOptions), cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,27 @@ internal static partial class ChatClientAgentLogMessages
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoking service {ServiceType}.")]
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoking client {ClientType}.")]
public static partial void LogAgentChatClientInvokingAgent(
this ILogger logger,
string methodName,
string agentId,
string agentName,
Type serviceType);
Type clientType);

/// <summary>
/// Logs <see cref="ChatClientAgent"/> invoked agent (complete).
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoked service {ServiceType} with message count: {MessageCount}.")]
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 serviceType,
Type clientType,
int messageCount);

/// <summary>
Expand All @@ -52,11 +52,11 @@ public static partial void LogAgentChatClientInvokedAgent(
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoked service {ServiceType}.")]
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoked client {ClientType}.")]
public static partial void LogAgentChatClientInvokedStreamingAgent(
this ILogger logger,
string methodName,
string agentId,
string agentName,
Type serviceType);
Type clientType);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Extensions.AI;

namespace Microsoft.Agents;

/// <summary>
Expand Down Expand Up @@ -30,4 +32,9 @@ public class ChatClientAgentOptions
/// Gets or sets the agent description.
/// </summary>
public string? Description { get; set; }

/// <summary>
/// Gets or sets the role used for agent instructions.
/// </summary>
public ChatRole? InstructionsRole { get; set; }
}
Loading
Loading