.NET: [BREAKING] Add ability to mark the source of Agent request messages and use that for filtering (#3540)

* Add ability to mark the source of Agent request messages and use that for filtering

* Add support for source, in addition to source type, and add unit tests for automatic stamping

* Address PR comments.

* Add merge fixes

* Address PR comments
This commit is contained in:
westey
2026-02-09 16:53:01 +00:00
committed by GitHub
parent 390f93344c
commit ad0dac3c86
34 changed files with 1718 additions and 384 deletions
@@ -62,7 +62,7 @@ namespace SampleApp
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
// Notify the session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages, storeMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages)
{
ResponseMessages = responseMessages
};
@@ -94,7 +94,7 @@ namespace SampleApp
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
// Notify the session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages, storeMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages)
{
ResponseMessages = responseMessages
};
@@ -104,7 +104,7 @@ namespace SampleApp
public UserInfo UserInfo { get; set; }
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
@@ -122,7 +122,7 @@ namespace SampleApp
}
}
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
StringBuilder instructions = new();
@@ -89,7 +89,7 @@ namespace SampleApp
public string? SessionDbKey { get; private set; }
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
@@ -107,7 +107,7 @@ namespace SampleApp
return messages;
}
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
// Don't store messages if the request failed.
if (context.InvokeException is not null)
@@ -122,7 +122,7 @@ namespace SampleApp
// Add both request and response messages to the store
// Optionally messages produced by the AIContextProvider can also be persisted (not shown).
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
{
@@ -92,7 +92,7 @@ namespace SampleApp
}
}
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
StringBuilder outputMessageBuilder = new();
outputMessageBuilder.AppendLine("Your todo list contains the following items:");
@@ -132,7 +132,7 @@ namespace SampleApp
/// </summary>
internal sealed class CalendarSearchAIContextProvider(Func<Task<string[]>> loadNextThreeCalendarEvents) : AIContextProvider
{
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var events = await loadNextThreeCalendarEvents();
@@ -179,7 +179,7 @@ namespace SampleApp
.ToList();
}
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
// Invoke all the sub providers.
var tasks = this._providers.Select(provider => provider.InvokingAsync(context, cancellationToken).AsTask());
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -31,6 +32,25 @@ namespace Microsoft.Agents.AI;
/// </remarks>
public abstract class AIContextProvider
{
private readonly string _sourceName;
/// <summary>
/// Initializes a new instance of the <see cref="AIContextProvider"/> class.
/// </summary>
protected AIContextProvider()
{
this._sourceName = this.GetType().FullName!;
}
/// <summary>
/// Initializes a new instance of the <see cref="AIContextProvider"/> class with the specified source name.
/// </summary>
/// <param name="sourceName">The source name to stamp on <see cref="ChatMessage.AdditionalProperties"/> for each messages produced by the <see cref="AIContextProvider"/>.</param>
protected AIContextProvider(string sourceName)
{
this._sourceName = sourceName;
}
/// <summary>
/// Called at the start of agent invocation to provide additional context.
/// </summary>
@@ -48,7 +68,57 @@ public abstract class AIContextProvider
/// </list>
/// </para>
/// </remarks>
public abstract ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default);
public async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var aiContext = await this.InvokingCoreAsync(context, cancellationToken).ConfigureAwait(false);
if (aiContext.Messages is null)
{
return aiContext;
}
aiContext.Messages = aiContext.Messages.Select(message =>
{
if (message.AdditionalProperties != null
// Check if the message was already tagged with this provider's source type
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out var messageSourceType)
&& messageSourceType is AgentRequestMessageSourceType typedMessageSourceType
&& typedMessageSourceType == AgentRequestMessageSourceType.AIContextProvider
// Check if the message was already tagged with this provider's source
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out var messageSource)
&& messageSource is string typedMessageSource
&& typedMessageSource == this._sourceName)
{
return message;
}
message = message.Clone();
message.AdditionalProperties ??= new();
message.AdditionalProperties[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.AIContextProvider;
message.AdditionalProperties[AgentRequestMessageSource.AdditionalPropertiesKey] = this._sourceName;
return message;
}).ToList();
return aiContext;
}
/// <summary>
/// Called at the start of agent invocation to provide additional context.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="AIContext"/> with additional context to be used by the agent during this invocation.</returns>
/// <remarks>
/// <para>
/// Implementers can load any additional context required at this time, such as:
/// <list type="bullet">
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
/// <item><description>Adding system instructions or prompts</description></item>
/// <item><description>Providing function tools for the current invocation</description></item>
/// <item><description>Injecting contextual messages from conversation history</description></item>
/// </list>
/// </para>
/// </remarks>
protected abstract ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default);
/// <summary>
/// Called at the end of the agent invocation to process the invocation results.
@@ -71,7 +141,31 @@ public abstract class AIContextProvider
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// </remarks>
public virtual ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> this.InvokedCoreAsync(context, cancellationToken);
/// <summary>
/// Called at the end of the agent invocation to process the invocation results.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <remarks>
/// <para>
/// Implementers can use the request and response messages in the provided <paramref name="context"/> to:
/// <list type="bullet">
/// <item><description>Update internal state based on conversation outcomes</description></item>
/// <item><description>Extract and store memories or preferences from user messages</description></item>
/// <item><description>Log or audit conversation details</description></item>
/// <item><description>Perform cleanup or finalization tasks</description></item>
/// </list>
/// </para>
/// <para>
/// This method is called regardless of whether the invocation succeeded or failed.
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// </remarks>
protected virtual ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> default;
/// <summary>
@@ -117,7 +211,7 @@ public abstract class AIContextProvider
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
/// <summary>
/// Contains the context information provided to <see cref="InvokingAsync(InvokingContext, CancellationToken)"/>.
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about the invocation before the underlying AI model is invoked, including the messages
@@ -163,7 +257,7 @@ public abstract class AIContextProvider
}
/// <summary>
/// Contains the context information provided to <see cref="InvokedAsync(InvokedContext, CancellationToken)"/>.
/// Contains the context information provided to <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about a completed agent invocation, including both the
@@ -178,18 +272,15 @@ public abstract class AIContextProvider
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">The caller provided messages that were used by the agent for this invocation.</param>
/// <param name="aiContextProviderMessages">The messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokedContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? aiContextProviderMessages)
IEnumerable<ChatMessage> requestMessages)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.AIContextProviderMessages = aiContextProviderMessages;
}
/// <summary>
@@ -211,15 +302,6 @@ public abstract class AIContextProvider
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }
/// <summary>
/// Gets the messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances that were provided by the <see cref="AIContextProvider"/>,
/// and were used by the agent as part of the invocation.
/// </value>
public IEnumerable<ChatMessage>? AIContextProviderMessages { get; set; }
/// <summary>
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
/// </summary>
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides a constant for the key used to store the source of the agent request message.
/// </summary>
public static class AgentRequestMessageSource
{
/// <summary>
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the source of the agent request message.
/// </summary>
public static readonly string AdditionalPropertiesKey = "Agent.RequestMessageSource";
}
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the source of an agent request message.
/// </summary>
/// <remarks>
/// Input messages for a specific agent run can originate from various sources.
/// This type helps to identify whether a message came from outside the agent pipeline,
/// whether it was produced by middleware, or came from chat history.
/// </remarks>
public sealed class AgentRequestMessageSourceType : IEquatable<AgentRequestMessageSourceType>
{
/// <summary>
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the source type of the agent request message.
/// </summary>
public static readonly string AdditionalPropertiesKey = "Agent.RequestMessageSourceType";
/// <summary>
/// Initializes a new instance of the <see cref="AgentRequestMessageSourceType"/> class.
/// </summary>
/// <param name="value">The string value representing the source of the agent request message.</param>
public AgentRequestMessageSourceType(string value) => this.Value = Throw.IfNullOrWhitespace(value);
/// <summary>
/// Get the string value representing the source of the agent request message.
/// </summary>
public string Value { get; }
/// <summary>
/// The message came from outside the agent pipeline (e.g., user input).
/// </summary>
public static AgentRequestMessageSourceType External { get; } = new AgentRequestMessageSourceType(nameof(External));
/// <summary>
/// The message was produced by middleware.
/// </summary>
public static AgentRequestMessageSourceType AIContextProvider { get; } = new AgentRequestMessageSourceType(nameof(AIContextProvider));
/// <summary>
/// The message came from chat history.
/// </summary>
public static AgentRequestMessageSourceType ChatHistory { get; } = new AgentRequestMessageSourceType(nameof(ChatHistory));
/// <summary>
/// Determines whether this instance and another specified <see cref="AgentRequestMessageSourceType"/> object have the same value.
/// </summary>
/// <param name="other">The <see cref="AgentRequestMessageSourceType"/> to compare to this instance.</param>
/// <returns><see langword="true"/> if the value of the <paramref name="other"/> parameter is the same as the value of this instance; otherwise, <see langword="false"/>.</returns>
public bool Equals(AgentRequestMessageSourceType? other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return string.Equals(this.Value, other.Value, StringComparison.Ordinal);
}
/// <summary>
/// Determines whether this instance and a specified object have the same value.
/// </summary>
/// <param name="obj">The object to compare to this instance.</param>
/// <returns><see langword="true"/> if <paramref name="obj"/> is a <see cref="AgentRequestMessageSourceType"/> and its value is the same as this instance; otherwise, <see langword="false"/>.</returns>
public override bool Equals(object? obj) => this.Equals(obj as AgentRequestMessageSourceType);
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer hash code.</returns>
public override int GetHashCode() => this.Value?.GetHashCode() ?? 0;
/// <summary>
/// Determines whether two specified <see cref="AgentRequestMessageSourceType"/> objects have the same value.
/// </summary>
/// <param name="left">The first <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <returns><see langword="true"/> if the value of <paramref name="left"/> is the same as the value of <paramref name="right"/>; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(AgentRequestMessageSourceType? left, AgentRequestMessageSourceType? right)
{
if (left is null)
{
return right is null;
}
return left.Equals(right);
}
/// <summary>
/// Determines whether two specified <see cref="AgentRequestMessageSourceType"/> objects have different values.
/// </summary>
/// <param name="left">The first <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <returns><see langword="true"/> if the value of <paramref name="left"/> is different from the value of <paramref name="right"/>; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(AgentRequestMessageSourceType? left, AgentRequestMessageSourceType? right) => !(left == right);
}
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -36,6 +37,25 @@ namespace Microsoft.Agents.AI;
/// </remarks>
public abstract class ChatHistoryProvider
{
private readonly string _sourceName;
/// <summary>
/// Initializes a new instance of the <see cref="ChatHistoryProvider"/> class.
/// </summary>
protected ChatHistoryProvider()
{
this._sourceName = this.GetType().FullName!;
}
/// <summary>
/// Initializes a new instance of the <see cref="ChatHistoryProvider"/> class with the specified source name.
/// </summary>
/// <param name="sourceName">The source name to stamp on <see cref="ChatMessage.AdditionalProperties"/> for each messages produced by the <see cref="ChatHistoryProvider"/>.</param>
protected ChatHistoryProvider(string sourceName)
{
this._sourceName = sourceName;
}
/// <summary>
/// Called at the start of agent invocation to provide messages from the chat history as context for the next agent invocation.
/// </summary>
@@ -65,7 +85,63 @@ public abstract class ChatHistoryProvider
/// and context management.
/// </para>
/// </remarks>
public abstract ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default);
public async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var messages = await this.InvokingCoreAsync(context, cancellationToken).ConfigureAwait(false);
return messages.Select(message =>
{
if (message.AdditionalProperties != null
// Check if the message was already tagged with this provider's source type
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out var messageSourceType)
&& messageSourceType is AgentRequestMessageSourceType typedMessageSourceType
&& typedMessageSourceType == AgentRequestMessageSourceType.ChatHistory
// Check if the message was already tagged with this provider's source
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out var messageSource)
&& messageSource is string typedMessageSource
&& typedMessageSource == this._sourceName)
{
return message;
}
message = message.Clone();
message.AdditionalProperties ??= new();
message.AdditionalProperties[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.ChatHistory;
message.AdditionalProperties[AgentRequestMessageSource.AdditionalPropertiesKey] = this._sourceName;
return message;
});
}
/// <summary>
/// Called at the start of agent invocation to provide messages from the chat history as context for the next agent invocation.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains a collection of <see cref="ChatMessage"/>
/// instances in ascending chronological order (oldest first).
/// </returns>
/// <remarks>
/// <para>
/// Messages are returned in chronological order to maintain proper conversation flow and context for the agent.
/// The oldest messages appear first in the collection, followed by more recent messages.
/// </para>
/// <para>
/// If the total message history becomes very large, implementations should apply appropriate strategies to manage
/// storage constraints, such as:
/// <list type="bullet">
/// <item><description>Truncating older messages while preserving recent context</description></item>
/// <item><description>Summarizing message groups to maintain essential context</description></item>
/// <item><description>Implementing sliding window approaches for message retention</description></item>
/// <item><description>Archiving old messages while keeping active conversation context</description></item>
/// </list>
/// </para>
/// <para>
/// Each <see cref="ChatHistoryProvider"/> instance should be associated with a single <see cref="AgentSession"/> to ensure proper message isolation
/// and context management.
/// </para>
/// </remarks>
protected abstract ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default);
/// <summary>
/// Called at the end of the agent invocation to add new messages to the chat history.
@@ -77,7 +153,7 @@ public abstract class ChatHistoryProvider
/// <para>
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
/// <see cref="InvokingAsync"/> return messages in the correct chronological order.
/// <see cref="InvokingCoreAsync"/> return messages in the correct chronological order.
/// </para>
/// <para>
/// Implementations may perform additional processing during message addition, such as:
@@ -92,7 +168,35 @@ public abstract class ChatHistoryProvider
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// </remarks>
public abstract ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default);
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
this.InvokedCoreAsync(context, cancellationToken);
/// <summary>
/// Called at the end of the agent invocation to add new messages to the chat history.
/// </summary>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous add operation.</returns>
/// <remarks>
/// <para>
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
/// <see cref="InvokingCoreAsync"/> return messages in the correct chronological order.
/// </para>
/// <para>
/// Implementations may perform additional processing during message addition, such as:
/// <list type="bullet">
/// <item><description>Validating message content and metadata</description></item>
/// <item><description>Applying storage optimizations or compression</description></item>
/// <item><description>Triggering background maintenance operations</description></item>
/// </list>
/// </para>
/// <para>
/// This method is called regardless of whether the invocation succeeded or failed.
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// </remarks>
protected abstract ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
@@ -131,7 +235,7 @@ public abstract class ChatHistoryProvider
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
/// <summary>
/// Contains the context information provided to <see cref="InvokingAsync(InvokingContext, CancellationToken)"/>.
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about the invocation including the new messages that will be used.
@@ -177,7 +281,7 @@ public abstract class ChatHistoryProvider
}
/// <summary>
/// Contains the context information provided to <see cref="InvokedAsync(InvokedContext, CancellationToken)"/>.
/// Contains the context information provided to <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about a completed agent invocation, including both the
@@ -192,18 +296,15 @@ public abstract class ChatHistoryProvider
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">The caller provided messages that were used by the agent for this invocation.</param>
/// <param name="chatHistoryProviderMessages">The messages retrieved from the <see cref="ChatHistoryProvider"/> for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokedContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? chatHistoryProviderMessages)
IEnumerable<ChatMessage> requestMessages)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.ChatHistoryProviderMessages = chatHistoryProviderMessages;
}
/// <summary>
@@ -225,24 +326,6 @@ public abstract class ChatHistoryProvider
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }
/// <summary>
/// Gets the messages retrieved from the <see cref="ChatHistoryProvider"/> for this invocation, if any.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances that were retrieved from the <see cref="ChatHistoryProvider"/>,
/// and were used by the agent as part of the invocation.
/// </value>
public IEnumerable<ChatMessage>? ChatHistoryProviderMessages { get; set; }
/// <summary>
/// Gets or sets the messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances that were provided by the <see cref="AIContextProvider"/>,
/// and were used by the agent as part of the invocation.
/// </value>
public IEnumerable<ChatMessage>? AIContextProviderMessages { get; set; }
/// <summary>
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
/// </summary>
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
@@ -33,8 +34,8 @@ public static class ChatHistoryProviderExtensions
}
/// <summary>
/// Decorates the provided chat message <see cref="ChatHistoryProvider"/> so that it does not add
/// messages produced by any <see cref="AIContextProvider"/> to chat history.
/// Decorates the provided <see cref="ChatHistoryProvider"/> so that it does not add
/// messages with <see cref="AgentRequestMessageSourceType.AIContextProvider"/> to chat history.
/// </summary>
/// <param name="provider">The <see cref="ChatHistoryProvider"/> to add the message filter to.</param>
/// <returns>A new <see cref="ChatHistoryProvider"/> instance that filters out <see cref="AIContextProvider"/> messages so they do not get added.</returns>
@@ -44,7 +45,7 @@ public static class ChatHistoryProviderExtensions
innerProvider: provider,
invokedMessagesFilter: (ctx) =>
{
ctx.AIContextProviderMessages = null;
ctx.RequestMessages = ctx.RequestMessages.Where(x => x.GetAgentRequestMessageSource() != AgentRequestMessageSourceType.AIContextProvider);
return ctx;
});
}
@@ -49,14 +49,14 @@ public sealed class ChatHistoryProviderMessageFilter : ChatHistoryProvider
}
/// <inheritdoc />
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var messages = await this._innerProvider.InvokingAsync(context, cancellationToken).ConfigureAwait(false);
return this._invokingMessagesFilter != null ? this._invokingMessagesFilter(messages) : messages;
}
/// <inheritdoc />
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
if (this._invokedMessagesFilter != null)
{
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Contains extension methods for <see cref="ChatMessage"/>
/// </summary>
public static class ChatMessageExtensions
{
/// <summary>
/// Gets the source of the provided <see cref="ChatMessage"/> in the context of messages passed into an agent run.
/// </summary>
/// <param name="message">The <see cref="ChatMessage"/> for which we need the source.</param>
/// <returns>An <see cref="AgentRequestMessageSourceType"/> value indicating the source of the <see cref="ChatMessage"/>. Defaults to <see
/// cref="AgentRequestMessageSourceType.External"/> if no explicit source is defined.</returns>
public static AgentRequestMessageSourceType GetAgentRequestMessageSource(this ChatMessage message)
{
if (message.AdditionalProperties?.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out var source) is true && source is AgentRequestMessageSourceType typedSource)
{
return typedSource;
}
return AgentRequestMessageSourceType.External;
}
}
@@ -133,7 +133,7 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider, IList<Cha
}
/// <inheritdoc />
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(context);
@@ -146,7 +146,7 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider, IList<Cha
}
/// <inheritdoc />
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(context);
@@ -155,8 +155,8 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider, IList<Cha
return;
}
// Add request, AI context provider, and response messages to the provider
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
// Add request and response messages to the provider
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
this._messages.AddRange(allNewMessages);
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
@@ -229,7 +229,7 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider, IList<Cha
{
/// <summary>
/// Trigger the reducer when a new message is added.
/// <see cref="InvokedAsync(InvokedContext, CancellationToken)"/> will only complete when reducer processing is done.
/// <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/> will only complete when reducer processing is done.
/// </summary>
AfterMessageAdded,
@@ -287,7 +287,7 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
}
/// <inheritdoc />
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
if (this._disposed)
@@ -347,7 +347,7 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
}
/// <inheritdoc />
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
Throw.IfNull(context);
@@ -364,7 +364,7 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
}
#pragma warning restore CA1513
var messageList = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []).ToList();
var messageList = context.RequestMessages.Concat(context.ResponseMessages ?? []).ToList();
if (messageList.Count == 0)
{
return;
@@ -131,13 +131,16 @@ public sealed class Mem0Provider : AIContextProvider
}
/// <inheritdoc />
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
Throw.IfNull(context);
string queryText = string.Join(
Environment.NewLine,
context.RequestMessages.Where(m => !string.IsNullOrWhiteSpace(m.Text)).Select(m => m.Text));
context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(m => !string.IsNullOrWhiteSpace(m.Text))
.Select(m => m.Text));
try
{
@@ -202,7 +205,7 @@ public sealed class Mem0Provider : AIContextProvider
}
/// <inheritdoc />
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
if (context.InvokeException is not null)
{
@@ -212,7 +215,11 @@ public sealed class Mem0Provider : AIContextProvider
try
{
// Persist request and response messages after invocation.
await this.PersistMessagesAsync(context.RequestMessages.Concat(context.ResponseMessages ?? []), cancellationToken).ConfigureAwait(false);
await this.PersistMessagesAsync(
context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Concat(context.ResponseMessages ?? []),
cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -46,17 +46,17 @@ internal sealed class WorkflowChatHistoryProvider : ChatHistoryProvider
internal void AddMessages(params IEnumerable<ChatMessage> messages) => this._chatMessages.AddRange(messages);
public override ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(this._chatMessages.AsReadOnly());
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
if (context.InvokeException is not null)
{
return default;
}
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
this._chatMessages.AddRange(allNewMessages);
return default;
@@ -206,9 +206,8 @@ public sealed partial class ChatClientAgent : AIAgent
(ChatClientAgentSession safeSession,
ChatOptions? chatOptions,
List<ChatMessage> inputMessagesForProviders,
List<ChatMessage> inputMessagesForChatClient,
IList<ChatMessage>? aiContextProviderMessages,
IList<ChatMessage>? chatHistoryProviderMessages,
ChatClientAgentContinuationToken? continuationToken) =
await this.PrepareSessionAndMessagesAsync(session, inputMessages, options, cancellationToken).ConfigureAwait(false);
@@ -227,12 +226,12 @@ public sealed partial class ChatClientAgent : AIAgent
try
{
// Using the enumerator to ensure we consider the case where no updates are returned for notification.
responseUpdatesEnumerator = chatClient.GetStreamingResponseAsync(inputMessagesForChatClient, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken);
responseUpdatesEnumerator = chatClient.GetStreamingResponseAsync(inputMessagesForProviders, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken);
}
catch (Exception ex)
{
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessagesForProviders, continuationToken), chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessagesForProviders, continuationToken), cancellationToken).ConfigureAwait(false);
throw;
}
@@ -246,8 +245,8 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessagesForProviders, continuationToken), chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessagesForProviders, continuationToken), cancellationToken).ConfigureAwait(false);
throw;
}
@@ -273,8 +272,8 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessagesForProviders, continuationToken), chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessagesForProviders, continuationToken), cancellationToken).ConfigureAwait(false);
throw;
}
}
@@ -286,10 +285,10 @@ public sealed partial class ChatClientAgent : AIAgent
await this.UpdateSessionWithTypeAndConversationIdAsync(safeSession, chatResponse.ConversationId, cancellationToken).ConfigureAwait(false);
// To avoid inconsistent state we only notify the session of the input messages if no error occurs after the initial request.
await this.NotifyChatHistoryProviderOfNewMessagesAsync(safeSession, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfNewMessagesAsync(safeSession, GetInputMessages(inputMessagesForProviders, continuationToken), chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
// Notify the AIContextProvider of all new messages.
await this.NotifyAIContextProviderOfSuccessAsync(safeSession, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfSuccessAsync(safeSession, GetInputMessages(inputMessagesForProviders, continuationToken), chatResponse.Messages, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -433,9 +432,8 @@ public sealed partial class ChatClientAgent : AIAgent
(ChatClientAgentSession safeSession,
ChatOptions? chatOptions,
List<ChatMessage> inputMessagesForProviders,
List<ChatMessage> inputMessagesForChatClient,
IList<ChatMessage>? aiContextProviderMessages,
IList<ChatMessage>? chatHistoryProviderMessages,
ChatClientAgentContinuationToken? _) =
await this.PrepareSessionAndMessagesAsync(session, inputMessages, options, cancellationToken).ConfigureAwait(false);
@@ -455,8 +453,8 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, inputMessages, chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, inputMessagesForProviders, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, inputMessagesForProviders, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -473,10 +471,10 @@ public sealed partial class ChatClientAgent : AIAgent
}
// Only notify the session of new messages if the chatResponse was successful to avoid inconsistent message state in the session.
await this.NotifyChatHistoryProviderOfNewMessagesAsync(safeSession, inputMessages, chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfNewMessagesAsync(safeSession, inputMessagesForProviders, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
// Notify the AIContextProvider of all new messages.
await this.NotifyAIContextProviderOfSuccessAsync(safeSession, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfSuccessAsync(safeSession, inputMessagesForProviders, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
var agentResponse = agentResponseFactoryFunc(chatResponse);
@@ -491,13 +489,12 @@ public sealed partial class ChatClientAgent : AIAgent
private async Task NotifyAIContextProviderOfSuccessAsync(
ChatClientAgentSession session,
IEnumerable<ChatMessage> inputMessages,
IList<ChatMessage>? aiContextProviderMessages,
IEnumerable<ChatMessage> responseMessages,
CancellationToken cancellationToken)
{
if (session.AIContextProvider is not null)
{
await session.AIContextProvider.InvokedAsync(new(this, session, inputMessages, aiContextProviderMessages) { ResponseMessages = responseMessages },
await session.AIContextProvider.InvokedAsync(new(this, session, inputMessages) { ResponseMessages = responseMessages },
cancellationToken).ConfigureAwait(false);
}
}
@@ -509,12 +506,11 @@ public sealed partial class ChatClientAgent : AIAgent
ChatClientAgentSession session,
Exception ex,
IEnumerable<ChatMessage> inputMessages,
IList<ChatMessage>? aiContextProviderMessages,
CancellationToken cancellationToken)
{
if (session.AIContextProvider is not null)
{
await session.AIContextProvider.InvokedAsync(new(this, session, inputMessages, aiContextProviderMessages) { InvokeException = ex },
await session.AIContextProvider.InvokedAsync(new(this, session, inputMessages) { InvokeException = ex },
cancellationToken).ConfigureAwait(false);
}
}
@@ -683,9 +679,8 @@ public sealed partial class ChatClientAgent : AIAgent
<(
ChatClientAgentSession AgentSession,
ChatOptions? ChatOptions,
List<ChatMessage> inputMessagesForProviders,
List<ChatMessage> InputMessagesForChatClient,
IList<ChatMessage>? AIContextProviderMessages,
IList<ChatMessage>? ChatHistoryProviderMessages,
ChatClientAgentContinuationToken? ContinuationToken
)> PrepareSessionAndMessagesAsync(
AgentSession? session,
@@ -714,9 +709,8 @@ public sealed partial class ChatClientAgent : AIAgent
throw new InvalidOperationException("Input messages are not allowed when continuing a background response using a continuation token.");
}
List<ChatMessage> inputMessagesForProviders = [];
List<ChatMessage> inputMessagesForChatClient = [];
IList<ChatMessage>? aiContextProviderMessages = null;
IList<ChatMessage>? chatHistoryProviderMessages = null;
// Populate the session messages only if we are not continuing an existing response as it's not allowed
if (chatOptions?.ContinuationToken is null)
@@ -729,10 +723,10 @@ public sealed partial class ChatClientAgent : AIAgent
var invokingContext = new ChatHistoryProvider.InvokingContext(this, typedSession, inputMessages);
var providerMessages = await chatHistoryProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
inputMessagesForChatClient.AddRange(providerMessages);
chatHistoryProviderMessages = providerMessages as IList<ChatMessage> ?? providerMessages.ToList();
}
// Add the input messages before getting context from AIContextProvider.
inputMessagesForProviders.AddRange(inputMessages);
inputMessagesForChatClient.AddRange(inputMessages);
// If we have an AIContextProvider, we should get context from it, and update our
@@ -743,8 +737,8 @@ public sealed partial class ChatClientAgent : AIAgent
var aiContext = await typedSession.AIContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
if (aiContext.Messages is { Count: > 0 })
{
inputMessagesForProviders.AddRange(aiContext.Messages);
inputMessagesForChatClient.AddRange(aiContext.Messages);
aiContextProviderMessages = aiContext.Messages;
}
if (aiContext.Tools is { Count: > 0 })
@@ -783,7 +777,7 @@ public sealed partial class ChatClientAgent : AIAgent
chatOptions.ConversationId = typedSession.ConversationId;
}
return (typedSession, chatOptions, inputMessagesForChatClient, aiContextProviderMessages, chatHistoryProviderMessages, continuationToken);
return (typedSession, chatOptions, inputMessagesForProviders, inputMessagesForChatClient, continuationToken);
}
private async Task UpdateSessionWithTypeAndConversationIdAsync(ChatClientAgentSession session, string? responseConversationId, CancellationToken cancellationToken)
@@ -816,8 +810,6 @@ public sealed partial class ChatClientAgent : AIAgent
ChatClientAgentSession session,
Exception ex,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? chatHistoryProviderMessages,
IEnumerable<ChatMessage>? aiContextProviderMessages,
ChatOptions? chatOptions,
CancellationToken cancellationToken)
{
@@ -827,9 +819,8 @@ public sealed partial class ChatClientAgent : AIAgent
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
if (provider is not null)
{
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, requestMessages, chatHistoryProviderMessages!)
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, requestMessages)
{
AIContextProviderMessages = aiContextProviderMessages,
InvokeException = ex
};
@@ -842,8 +833,6 @@ public sealed partial class ChatClientAgent : AIAgent
private Task NotifyChatHistoryProviderOfNewMessagesAsync(
ChatClientAgentSession session,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? chatHistoryProviderMessages,
IEnumerable<ChatMessage>? aiContextProviderMessages,
IEnumerable<ChatMessage> responseMessages,
ChatOptions? chatOptions,
CancellationToken cancellationToken)
@@ -854,9 +843,8 @@ public sealed partial class ChatClientAgent : AIAgent
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
if (provider is not null)
{
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, requestMessages, chatHistoryProviderMessages!)
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, requestMessages)
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
};
return provider.InvokedAsync(invokedContext, cancellationToken).AsTask();
@@ -25,8 +25,8 @@ namespace Microsoft.Agents.AI;
/// abstractions to work with any compatible vector store implementation.
/// </para>
/// <para>
/// Messages are stored during the <see cref="InvokedAsync"/> method and retrieved during the
/// <see cref="InvokingAsync"/> method using semantic similarity search.
/// Messages are stored during the <see cref="InvokedCoreAsync"/> method and retrieved during the
/// <see cref="InvokingCoreAsync"/> method using semantic similarity search.
/// </para>
/// <para>
/// Behavior is configurable through <see cref="ChatHistoryMemoryProviderOptions"/>. When
@@ -175,7 +175,7 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
}
/// <inheritdoc />
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(context);
@@ -189,6 +189,7 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
{
// Get the text from the current request messages
var requestText = string.Join("\n", context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(m => m != null && !string.IsNullOrWhiteSpace(m.Text))
.Select(m => m.Text));
@@ -228,7 +229,7 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
}
/// <inheritdoc />
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(context);
@@ -244,6 +245,7 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
var collection = await this.EnsureCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
List<Dictionary<string, object?>> itemsToStore = context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Concat(context.ResponseMessages ?? [])
.Select(message => new Dictionary<string, object?>
{
@@ -107,7 +107,7 @@ public sealed class TextSearchProvider : AIContextProvider
}
/// <inheritdoc />
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
if (this._searchTime != TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke)
{
@@ -117,7 +117,9 @@ public sealed class TextSearchProvider : AIContextProvider
// Aggregate text from memory + current request messages.
var sbInput = new StringBuilder();
var requestMessagesText = context.RequestMessages.Where(x => !string.IsNullOrWhiteSpace(x?.Text)).Select(x => x.Text);
var requestMessagesText = context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(x => !string.IsNullOrWhiteSpace(x?.Text)).Select(x => x.Text);
foreach (var messageText in this._recentMessagesText.Concat(requestMessagesText))
{
if (sbInput.Length > 0)
@@ -166,7 +168,7 @@ public sealed class TextSearchProvider : AIContextProvider
}
/// <inheritdoc />
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
int limit = this._recentMessageMemoryLimit;
if (limit <= 0)
@@ -180,6 +182,7 @@ public sealed class TextSearchProvider : AIContextProvider
}
var messagesText = context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Concat(context.ResponseMessages ?? [])
.Where(m =>
this._recentMessageRolesIncluded.Contains(m.Role) &&
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -15,35 +16,155 @@ public class AIContextProviderTests
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
#region InvokingAsync Message Stamping Tests
[Fact]
public async Task InvokingAsync_StampsMessagesWithSourceTypeAndSourceAsync()
{
// Arrange
var provider = new TestAIContextProviderWithMessages();
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
AIContext aiContext = await provider.InvokingAsync(context);
// Assert
Assert.NotNull(aiContext.Messages);
ChatMessage message = aiContext.Messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestAIContextProviderWithMessages).FullName, source);
}
[Fact]
public async Task InvokingAsync_WithCustomSourceName_StampsMessagesWithCustomSourceAsync()
{
// Arrange
const string CustomSourceName = "CustomContextSource";
var provider = new TestAIContextProviderWithCustomSource(CustomSourceName);
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
AIContext aiContext = await provider.InvokingAsync(context);
// Assert
Assert.NotNull(aiContext.Messages);
ChatMessage message = aiContext.Messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(CustomSourceName, source);
}
[Fact]
public async Task InvokingAsync_DoesNotReStampAlreadyStampedMessagesAsync()
{
// Arrange
var provider = new TestAIContextProviderWithPreStampedMessages();
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
AIContext aiContext = await provider.InvokingAsync(context);
// Assert
Assert.NotNull(aiContext.Messages);
ChatMessage message = aiContext.Messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestAIContextProviderWithPreStampedMessages).FullName, source);
}
[Fact]
public async Task InvokingAsync_StampsMultipleMessagesAsync()
{
// Arrange
var provider = new TestAIContextProviderWithMultipleMessages();
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
AIContext aiContext = await provider.InvokingAsync(context);
// Assert
Assert.NotNull(aiContext.Messages);
List<ChatMessage> messageList = aiContext.Messages.ToList();
Assert.Equal(3, messageList.Count);
foreach (ChatMessage message in messageList)
{
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestAIContextProviderWithMultipleMessages).FullName, source);
}
}
[Fact]
public async Task InvokingAsync_WithNullMessages_ReturnsContextWithoutStampingAsync()
{
// Arrange
var provider = new TestAIContextProvider();
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
AIContext aiContext = await provider.InvokingAsync(context);
// Assert
Assert.Null(aiContext.Messages);
}
#endregion
#region Basic Tests
[Fact]
public async Task InvokedAsync_ReturnsCompletedTaskAsync()
{
// Arrange
var provider = new TestAIContextProvider();
var messages = new ReadOnlyCollection<ChatMessage>([]);
var task = provider.InvokedAsync(new(s_mockAgent, s_mockSession, messages, aiContextProviderMessages: null));
// Act
ValueTask task = provider.InvokedAsync(new(s_mockAgent, s_mockSession, messages));
// Assert
Assert.Equal(default, task);
}
[Fact]
public void Serialize_ReturnsEmptyElement()
{
// Arrange
var provider = new TestAIContextProvider();
// Act
var actual = provider.Serialize();
// Assert
Assert.Equal(default, actual);
}
[Fact]
public void InvokingContext_Constructor_ThrowsForNullMessages()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, null!));
}
[Fact]
public void InvokedContext_Constructor_ThrowsForNullMessages()
{
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, null!, aiContextProviderMessages: null));
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, null!));
}
#endregion
#region GetService Method Tests
/// <summary>
@@ -246,7 +367,7 @@ public class AIContextProviderTests
{
// Arrange
var messages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, messages, aiContextProviderMessages: null);
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, messages);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => context.RequestMessages = null!);
@@ -258,7 +379,7 @@ public class AIContextProviderTests
// Arrange
var initialMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var newMessages = new List<ChatMessage> { new(ChatRole.User, "New message") };
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, initialMessages, aiContextProviderMessages: null);
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, initialMessages);
// Act
context.RequestMessages = newMessages;
@@ -267,28 +388,13 @@ public class AIContextProviderTests
Assert.Same(newMessages, context.RequestMessages);
}
[Fact]
public void InvokedContext_AIContextProviderMessages_Roundtrips()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var aiContextMessages = new List<ChatMessage> { new(ChatRole.System, "AI context message") };
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
// Act
context.AIContextProviderMessages = aiContextMessages;
// Assert
Assert.Same(aiContextMessages, context.AIContextProviderMessages);
}
[Fact]
public void InvokedContext_ResponseMessages_Roundtrips()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response message") };
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Act
context.ResponseMessages = responseMessages;
@@ -303,7 +409,7 @@ public class AIContextProviderTests
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var exception = new InvalidOperationException("Test exception");
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Act
context.InvokeException = exception;
@@ -319,7 +425,7 @@ public class AIContextProviderTests
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Assert
Assert.Same(s_mockAgent, context.Agent);
@@ -332,7 +438,7 @@ public class AIContextProviderTests
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Assert
Assert.Same(s_mockSession, context.Session);
@@ -345,7 +451,7 @@ public class AIContextProviderTests
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, null, requestMessages, aiContextProviderMessages: null);
var context = new AIContextProvider.InvokedContext(s_mockAgent, null, requestMessages);
// Assert
Assert.Null(context.Session);
@@ -358,16 +464,66 @@ public class AIContextProviderTests
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(null!, s_mockSession, requestMessages, aiContextProviderMessages: null));
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(null!, s_mockSession, requestMessages));
}
#endregion
private sealed class TestAIContextProvider : AIContextProvider
{
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(new AIContext());
}
private sealed class TestAIContextProviderWithMessages : AIContextProvider
{
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(new AIContext
{
Messages = [new ChatMessage(ChatRole.System, "Context Message")]
});
}
private sealed class TestAIContextProviderWithCustomSource : AIContextProvider
{
public TestAIContextProviderWithCustomSource(string sourceName) : base(sourceName)
{
return default;
}
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(new AIContext
{
Messages = [new ChatMessage(ChatRole.System, "Context Message")]
});
}
private sealed class TestAIContextProviderWithPreStampedMessages : AIContextProvider
{
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var message = new ChatMessage(ChatRole.System, "Pre-stamped Message");
message.AdditionalProperties = new AdditionalPropertiesDictionary
{
[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.AIContextProvider,
[AgentRequestMessageSource.AdditionalPropertiesKey] = this.GetType().FullName!
};
return new(new AIContext
{
Messages = [message]
});
}
}
private sealed class TestAIContextProviderWithMultipleMessages : AIContextProvider
{
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(new AIContext
{
Messages = [
new ChatMessage(ChatRole.System, "Message 1"),
new ChatMessage(ChatRole.User, "Message 2"),
new ChatMessage(ChatRole.Assistant, "Message 3")
]
});
}
}
@@ -0,0 +1,489 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AgentRequestMessageSourceType"/> class.
/// </summary>
public sealed class AgentRequestMessageSourceTypeTests
{
#region Constructor Tests
[Fact]
public void Constructor_WithValue_SetsValueProperty()
{
// Arrange
const string ExpectedValue = "CustomSource";
// Act
AgentRequestMessageSourceType source = new(ExpectedValue);
// Assert
Assert.Equal(ExpectedValue, source.Value);
}
[Fact]
public void Constructor_WithNullValue_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentRequestMessageSourceType(null!));
}
[Fact]
public void Constructor_WithEmptyValue_Throws()
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new AgentRequestMessageSourceType(string.Empty));
}
#endregion
#region Static Properties Tests
[Fact]
public void External_ReturnsInstanceWithExternalValue()
{
// Arrange & Act
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.External;
// Assert
Assert.NotNull(source);
Assert.Equal("External", source.Value);
}
[Fact]
public void AIContextProvider_ReturnsInstanceWithAIContextProviderValue()
{
// Arrange & Act
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.AIContextProvider;
// Assert
Assert.NotNull(source);
Assert.Equal("AIContextProvider", source.Value);
}
[Fact]
public void ChatHistory_ReturnsInstanceWithChatHistoryValue()
{
// Arrange & Act
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.ChatHistory;
// Assert
Assert.NotNull(source);
Assert.Equal("ChatHistory", source.Value);
}
[Fact]
public void AdditionalPropertiesKey_ReturnsExpectedValue()
{
// Arrange & Act
string key = AgentRequestMessageSourceType.AdditionalPropertiesKey;
// Assert
Assert.Equal("Agent.RequestMessageSourceType", key);
}
[Fact]
public void StaticProperties_ReturnSameInstanceOnMultipleCalls()
{
// Arrange & Act
AgentRequestMessageSourceType external1 = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType external2 = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType aiContextProvider1 = AgentRequestMessageSourceType.AIContextProvider;
AgentRequestMessageSourceType aiContextProvider2 = AgentRequestMessageSourceType.AIContextProvider;
AgentRequestMessageSourceType chatHistory1 = AgentRequestMessageSourceType.ChatHistory;
AgentRequestMessageSourceType chatHistory2 = AgentRequestMessageSourceType.ChatHistory;
// Assert
Assert.Same(external1, external2);
Assert.Same(aiContextProvider1, aiContextProvider2);
Assert.Same(chatHistory1, chatHistory2);
}
#endregion
#region Equals Tests
[Fact]
public void Equals_WithSameInstance_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
// Act
bool result = source.Equals(source);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithEqualValue_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1.Equals(source2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithDifferentValue_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test1");
AgentRequestMessageSourceType source2 = new("Test2");
// Act
bool result = source1.Equals(source2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithNull_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
// Act
bool result = source.Equals(null);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithDifferentCase_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("test");
// Act
bool result = source1.Equals(source2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_StaticExternalWithNewInstanceHavingSameValue_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType external = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType newExternal = new("External");
// Act
bool result = external.Equals(newExternal);
// Assert
Assert.True(result);
}
#endregion
#region Object.Equals Tests
[Fact]
public void ObjectEquals_WithEqualAgentRequestMessageSource_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
object source2 = new AgentRequestMessageSourceType("Test");
// Act
bool result = source1.Equals(source2);
// Assert
Assert.True(result);
}
[Fact]
public void ObjectEquals_WithDifferentType_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
object other = "Test";
// Act
bool result = source.Equals(other);
// Assert
Assert.False(result);
}
[Fact]
public void ObjectEquals_WithNullObject_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
object? other = null;
// Act
bool result = source.Equals(other);
// Assert
Assert.False(result);
}
#endregion
#region GetHashCode Tests
[Fact]
public void GetHashCode_WithSameValue_ReturnsSameHashCode()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act
int hashCode1 = source1.GetHashCode();
int hashCode2 = source2.GetHashCode();
// Assert
Assert.Equal(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_WithDifferentValue_ReturnsDifferentHashCode()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test1");
AgentRequestMessageSourceType source2 = new("Test2");
// Act
int hashCode1 = source1.GetHashCode();
int hashCode2 = source2.GetHashCode();
// Assert
Assert.NotEqual(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_ConsistentWithEquals()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act & Assert
// If two objects are equal, they must have the same hash code
Assert.True(source1.Equals(source2));
Assert.Equal(source1.GetHashCode(), source2.GetHashCode());
}
#endregion
#region Equality Operator Tests
[Fact]
public void EqualityOperator_WithEqualValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1 == source2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithDifferentValues_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test1");
AgentRequestMessageSourceType source2 = new("Test2");
// Act
bool result = source1 == source2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithBothNull_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType? source1 = null;
AgentRequestMessageSourceType? source2 = null;
// Act
bool result = source1 == source2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithLeftNull_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType? source1 = null;
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1 == source2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithRightNull_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType? source2 = null;
// Act
bool result = source1 == source2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithStaticInstances_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType external1 = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType external2 = AgentRequestMessageSourceType.External;
// Act
bool result = external1 == external2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_StaticWithNewInstanceHavingSameValue_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType external = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType newExternal = new("External");
// Act
bool result = external == newExternal;
// Assert
Assert.True(result);
}
#endregion
#region Inequality Operator Tests
[Fact]
public void InequalityOperator_WithEqualValues_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1 != source2;
// Assert
Assert.False(result);
}
[Fact]
public void InequalityOperator_WithDifferentValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test1");
AgentRequestMessageSourceType source2 = new("Test2");
// Act
bool result = source1 != source2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_WithBothNull_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType? source1 = null;
AgentRequestMessageSourceType? source2 = null;
// Act
bool result = source1 != source2;
// Assert
Assert.False(result);
}
[Fact]
public void InequalityOperator_WithLeftNull_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType? source1 = null;
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1 != source2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_WithRightNull_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType? source2 = null;
// Act
bool result = source1 != source2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_DifferentStaticInstances_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType external = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType chatHistory = AgentRequestMessageSourceType.ChatHistory;
// Act
bool result = external != chatHistory;
// Assert
Assert.True(result);
}
#endregion
#region IEquatable Tests
[Fact]
public void IEquatable_ImplementedCorrectly()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
// Act & Assert
Assert.IsAssignableFrom<IEquatable<AgentRequestMessageSourceType>>(source);
}
#endregion
}
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -41,7 +42,8 @@ public sealed class ChatHistoryProviderExtensionsTests
ChatHistoryProvider.InvokingContext context = new(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
providerMock
.Setup(p => p.InvokingAsync(context, It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(innerMessages);
ChatHistoryProvider filtered = providerMock.Object.WithMessageFilters(
@@ -60,16 +62,20 @@ public sealed class ChatHistoryProviderExtensionsTests
{
// Arrange
Mock<ChatHistoryProvider> providerMock = new();
List<ChatMessage> requestMessages = [new(ChatRole.User, "Hello")];
List<ChatMessage> chatHistoryProviderMessages = [new(ChatRole.System, "System")];
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages, chatHistoryProviderMessages)
List<ChatMessage> requestMessages =
[
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory } } },
new(ChatRole.User, "Hello")
];
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages)
{
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
};
ChatHistoryProvider.InvokedContext? capturedContext = null;
providerMock
.Setup(p => p.InvokedAsync(It.IsAny<ChatHistoryProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Callback<ChatHistoryProvider.InvokedContext, CancellationToken>((ctx, _) => capturedContext = ctx)
.Returns(default(ValueTask));
@@ -106,17 +112,18 @@ public sealed class ChatHistoryProviderExtensionsTests
{
// Arrange
Mock<ChatHistoryProvider> providerMock = new();
List<ChatMessage> requestMessages = [new(ChatRole.User, "Hello")];
List<ChatMessage> chatHistoryProviderMessages = [new(ChatRole.System, "System")];
List<ChatMessage> aiContextProviderMessages = [new(ChatRole.System, "Context")];
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages, chatHistoryProviderMessages)
{
AIContextProviderMessages = aiContextProviderMessages
};
List<ChatMessage> requestMessages =
[
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory } } },
new(ChatRole.User, "Hello"),
new(ChatRole.System, "Context") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.AIContextProvider } } }
];
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages);
ChatHistoryProvider.InvokedContext? capturedContext = null;
providerMock
.Setup(p => p.InvokedAsync(It.IsAny<ChatHistoryProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Callback<ChatHistoryProvider.InvokedContext, CancellationToken>((ctx, _) => capturedContext = ctx)
.Returns(default(ValueTask));
@@ -127,6 +134,8 @@ public sealed class ChatHistoryProviderExtensionsTests
// Assert
Assert.NotNull(capturedContext);
Assert.Null(capturedContext.AIContextProviderMessages);
Assert.Equal(2, capturedContext.RequestMessages.Count());
Assert.Contains("System", capturedContext.RequestMessages.Select(x => x.Text));
Assert.Contains("Hello", capturedContext.RequestMessages.Select(x => x.Text));
}
}
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -65,7 +66,8 @@ public sealed class ChatHistoryProviderMessageFilterTests
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
innerProviderMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(expectedMessages);
var filter = new ChatHistoryProviderMessageFilter(innerProviderMock.Object, x => x, x => x);
@@ -77,7 +79,9 @@ public sealed class ChatHistoryProviderMessageFilterTests
Assert.Equal(2, result.Count);
Assert.Equal("Hello", result[0].Text);
Assert.Equal("Hi there!", result[1].Text);
innerProviderMock.Verify(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()), Times.Once);
innerProviderMock
.Protected()
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
}
[Fact]
@@ -94,7 +98,8 @@ public sealed class ChatHistoryProviderMessageFilterTests
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
innerProviderMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(innerMessages);
// Filter to only user messages
@@ -108,7 +113,9 @@ public sealed class ChatHistoryProviderMessageFilterTests
// Assert
Assert.Equal(2, result.Count);
Assert.All(result, msg => Assert.Equal(ChatRole.User, msg.Role));
innerProviderMock.Verify(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()), Times.Once);
innerProviderMock
.Protected()
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
}
[Fact]
@@ -124,7 +131,8 @@ public sealed class ChatHistoryProviderMessageFilterTests
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
innerProviderMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(innerMessages);
// Filter that transforms messages
@@ -147,28 +155,31 @@ public sealed class ChatHistoryProviderMessageFilterTests
{
// Arrange
var innerProviderMock = new Mock<ChatHistoryProvider>();
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var chatHistoryProviderMessages = new List<ChatMessage> { new(ChatRole.System, "System") };
List<ChatMessage> requestMessages =
[
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory } } },
new(ChatRole.User, "Hello"),
];
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, chatHistoryProviderMessages)
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages)
{
ResponseMessages = responseMessages
};
ChatHistoryProvider.InvokedContext? capturedContext = null;
innerProviderMock
.Setup(s => s.InvokedAsync(It.IsAny<ChatHistoryProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Callback<ChatHistoryProvider.InvokedContext, CancellationToken>((ctx, ct) => capturedContext = ctx)
.Returns(default(ValueTask));
// Filter that modifies the context
ChatHistoryProvider.InvokedContext InvokedFilter(ChatHistoryProvider.InvokedContext ctx)
{
var modifiedRequestMessages = ctx.RequestMessages.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList();
return new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, modifiedRequestMessages, ctx.ChatHistoryProviderMessages)
var modifiedRequestMessages = ctx.RequestMessages.Where(x => x.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External).Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList();
return new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, modifiedRequestMessages)
{
ResponseMessages = ctx.ResponseMessages,
AIContextProviderMessages = ctx.AIContextProviderMessages,
InvokeException = ctx.InvokeException
};
}
@@ -182,7 +193,9 @@ public sealed class ChatHistoryProviderMessageFilterTests
Assert.NotNull(capturedContext);
Assert.Single(capturedContext.RequestMessages);
Assert.Equal("[FILTERED] Hello", capturedContext.RequestMessages.First().Text);
innerProviderMock.Verify(s => s.InvokedAsync(It.IsAny<ChatHistoryProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
innerProviderMock
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>());
}
[Fact]
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -18,6 +19,92 @@ public class ChatHistoryProviderTests
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
#region InvokingAsync Message Stamping Tests
[Fact]
public async Task InvokingAsync_StampsMessagesWithSourceTypeAndSourceAsync()
{
// Arrange
var provider = new TestChatHistoryProvider();
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
IEnumerable<ChatMessage> messages = await provider.InvokingAsync(context);
// Assert
ChatMessage message = messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestChatHistoryProvider).FullName, source);
}
[Fact]
public async Task InvokingAsync_WithCustomSourceName_StampsMessagesWithCustomSourceAsync()
{
// Arrange
const string CustomSourceName = "CustomHistorySource";
var provider = new TestChatHistoryProviderWithCustomSource(CustomSourceName);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
IEnumerable<ChatMessage> messages = await provider.InvokingAsync(context);
// Assert
ChatMessage message = messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(CustomSourceName, source);
}
[Fact]
public async Task InvokingAsync_DoesNotReStampAlreadyStampedMessagesAsync()
{
// Arrange
var provider = new TestChatHistoryProviderWithPreStampedMessages();
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
IEnumerable<ChatMessage> messages = await provider.InvokingAsync(context);
// Assert
ChatMessage message = messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestChatHistoryProviderWithPreStampedMessages).FullName, source);
}
[Fact]
public async Task InvokingAsync_StampsMultipleMessagesAsync()
{
// Arrange
var provider = new TestChatHistoryProviderWithMultipleMessages();
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
IEnumerable<ChatMessage> messages = await provider.InvokingAsync(context);
// Assert
List<ChatMessage> messageList = messages.ToList();
Assert.Equal(3, messageList.Count);
foreach (ChatMessage message in messageList)
{
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestChatHistoryProviderWithMultipleMessages).FullName, source);
}
}
#endregion
#region GetService Method Tests
[Fact]
@@ -172,7 +259,7 @@ public class ChatHistoryProviderTests
public void InvokedContext_Constructor_ThrowsForNullRequestMessages()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, null!, []));
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, null!));
}
[Fact]
@@ -180,7 +267,7 @@ public class ChatHistoryProviderTests
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => context.RequestMessages = null!);
@@ -192,7 +279,7 @@ public class ChatHistoryProviderTests
// Arrange
var initialMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var newMessages = new List<ChatMessage> { new(ChatRole.User, "New message") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, initialMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, initialMessages);
// Act
context.RequestMessages = newMessages;
@@ -201,43 +288,13 @@ public class ChatHistoryProviderTests
Assert.Same(newMessages, context.RequestMessages);
}
[Fact]
public void InvokedContext_ChatHistoryProviderMessages_SetterRoundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var newProviderMessages = new List<ChatMessage> { new(ChatRole.System, "System message") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Act
context.ChatHistoryProviderMessages = newProviderMessages;
// Assert
Assert.Same(newProviderMessages, context.ChatHistoryProviderMessages);
}
[Fact]
public void InvokedContext_AIContextProviderMessages_Roundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var aiContextMessages = new List<ChatMessage> { new(ChatRole.System, "AI context message") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Act
context.AIContextProviderMessages = aiContextMessages;
// Assert
Assert.Same(aiContextMessages, context.AIContextProviderMessages);
}
[Fact]
public void InvokedContext_ResponseMessages_Roundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response message") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Act
context.ResponseMessages = responseMessages;
@@ -252,7 +309,7 @@ public class ChatHistoryProviderTests
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var exception = new InvalidOperationException("Test exception");
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Act
context.InvokeException = exception;
@@ -268,7 +325,7 @@ public class ChatHistoryProviderTests
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Assert
Assert.Same(s_mockAgent, context.Agent);
@@ -281,7 +338,7 @@ public class ChatHistoryProviderTests
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages);
// Assert
Assert.Same(s_mockSession, context.Session);
@@ -294,7 +351,7 @@ public class ChatHistoryProviderTests
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, null, requestMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, null, requestMessages);
// Assert
Assert.Null(context.Session);
@@ -307,17 +364,69 @@ public class ChatHistoryProviderTests
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(null!, s_mockSession, requestMessages, []));
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(null!, s_mockSession, requestMessages));
}
#endregion
private sealed class TestChatHistoryProvider : ChatHistoryProvider
{
public override ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(Array.Empty<ChatMessage>());
protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new([new ChatMessage(ChatRole.User, "Test Message")]);
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> default;
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> default;
}
private sealed class TestChatHistoryProviderWithCustomSource : ChatHistoryProvider
{
public TestChatHistoryProviderWithCustomSource(string sourceName) : base(sourceName)
{
}
protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new([new ChatMessage(ChatRole.User, "Test Message")]);
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> default;
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> default;
}
private sealed class TestChatHistoryProviderWithPreStampedMessages : ChatHistoryProvider
{
protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var message = new ChatMessage(ChatRole.User, "Pre-stamped Message");
message.AdditionalProperties = new AdditionalPropertiesDictionary
{
[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.ChatHistory,
[AgentRequestMessageSource.AdditionalPropertiesKey] = this.GetType().FullName!
};
return new([message]);
}
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> default;
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> default;
}
private sealed class TestChatHistoryProviderWithMultipleMessages : ChatHistoryProvider
{
protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new([
new ChatMessage(ChatRole.User, "Message 1"),
new ChatMessage(ChatRole.Assistant, "Message 2"),
new ChatMessage(ChatRole.User, "Message 3")
]);
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> default;
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
@@ -0,0 +1,197 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="ChatMessageExtensions"/> class.
/// </summary>
public sealed class ChatMessageExtensionsTests
{
#region GetAgentRequestMessageSource Tests
[Fact]
public void GetAgentRequestMessageSource_WithNoAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithNullAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = null
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithEmptyAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary()
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithExternalSource_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.External }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithAIContextProviderSource_ReturnsAIContextProvider()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.AIContextProvider }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithChatHistorySource_ReturnsChatHistory()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithCustomSource_ReturnsCustomSource()
{
// Arrange
AgentRequestMessageSourceType customSource = new("CustomSource");
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, customSource }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(customSource, result);
Assert.Equal("CustomSource", result.Value);
}
[Fact]
public void GetAgentRequestMessageSource_WithWrongKeyType_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, "NotAnAgentRequestMessageSource" }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithNullValue_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, null! }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithMultipleProperties_ReturnsCorrectSource()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ "OtherProperty", "SomeValue" },
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory },
{ "AnotherProperty", 123 }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
// Assert
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result);
}
#endregion
}
@@ -54,7 +54,8 @@ public class InMemoryChatHistoryProviderTests
{
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello")
new(ChatRole.User, "Hello"),
new(ChatRole.System, "additional context") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory } } },
};
var responseMessages = new List<ChatMessage>
{
@@ -64,16 +65,11 @@ public class InMemoryChatHistoryProviderTests
{
new(ChatRole.System, "original instructions")
};
var aiContextProviderMessages = new List<ChatMessage>()
{
new(ChatRole.System, "additional context")
};
var provider = new InMemoryChatHistoryProvider();
provider.Add(providerMessages[0]);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, providerMessages)
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages)
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
};
await provider.InvokedAsync(context, CancellationToken.None);
@@ -90,7 +86,7 @@ public class InMemoryChatHistoryProviderTests
{
var provider = new InMemoryChatHistoryProvider();
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [], []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, []);
await provider.InvokedAsync(context, CancellationToken.None);
Assert.Empty(provider);
@@ -186,7 +182,7 @@ public class InMemoryChatHistoryProviderTests
var provider = new InMemoryChatHistoryProvider();
var messages = new List<ChatMessage>();
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages);
await provider.InvokedAsync(context, CancellationToken.None);
Assert.Empty(provider);
@@ -523,7 +519,7 @@ public class InMemoryChatHistoryProviderTests
var provider = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.AfterMessageAdded);
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, originalMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, originalMessages);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
@@ -582,7 +578,7 @@ public class InMemoryChatHistoryProviderTests
var provider = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.BeforeMessagesRetrieval);
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, originalMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, originalMessages);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
@@ -630,7 +626,7 @@ public class InMemoryChatHistoryProviderTests
{
new(ChatRole.Assistant, "Hi there!")
};
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, [])
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages)
{
ResponseMessages = responseMessages,
InvokeException = new InvalidOperationException("Test exception")
@@ -3140,7 +3140,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
/// </summary>
private sealed class TestAIContextProvider : AIContextProvider
{
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return new ValueTask<AIContext>(new AIContext());
}
@@ -3151,12 +3151,12 @@ public sealed class AzureAIProjectChatClientExtensionsTests
/// </summary>
private sealed class TestChatHistoryProvider : ChatHistoryProvider
{
public override ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return new ValueTask<IEnumerable<ChatMessage>>(Array.Empty<ChatMessage>());
}
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
return default;
}
@@ -217,7 +217,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
var message = new ChatMessage(ChatRole.User, "Hello, world!");
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [message], [])
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [message])
{
ResponseMessages = []
};
@@ -285,20 +285,16 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
{
new ChatMessage(ChatRole.User, "First message"),
new ChatMessage(ChatRole.Assistant, "Second message"),
new ChatMessage(ChatRole.User, "Third message")
};
var aiContextProviderMessages = new[]
{
new ChatMessage(ChatRole.System, "System context message")
new ChatMessage(ChatRole.User, "Third message"),
new ChatMessage(ChatRole.System, "System context message") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.AIContextProvider } } }
};
var responseMessages = new[]
{
new ChatMessage(ChatRole.Assistant, "Response message")
};
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, [])
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages)
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
};
@@ -349,8 +345,8 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var store1 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversation1);
using var store2 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversation2);
var context1 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message for conversation 1")], []);
var context2 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message for conversation 2")], []);
var context1 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message for conversation 1")]);
var context2 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message for conversation 2")]);
await store1.InvokedAsync(context1);
await store2.InvokedAsync(context2);
@@ -394,7 +390,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
};
// Act 1: Add messages
var invokedContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
var invokedContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages);
await originalStore.InvokedAsync(invokedContext);
// Act 2: Verify messages were added
@@ -548,7 +544,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
var message = new ChatMessage(ChatRole.User, "Hello from hierarchical partitioning!");
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [message], []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [message]);
// Act
await provider.InvokedAsync(context);
@@ -605,7 +601,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
new ChatMessage(ChatRole.User, "Third hierarchical message")
};
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages);
// Act
await provider.InvokedAsync(context);
@@ -640,8 +636,8 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var store2 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId2, SessionId);
// Add messages to both stores
var context1 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message from user 1")], []);
var context2 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message from user 2")], []);
var context1 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message from user 1")]);
var context2 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message from user 2")]);
await store1.InvokedAsync(context1);
await store2.InvokedAsync(context2);
@@ -678,7 +674,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var originalStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test serialization message")], []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test serialization message")]);
await originalStore.InvokedAsync(context);
// Act - Serialize the provider state
@@ -720,8 +716,8 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var hierarchicalProvider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-coexist", "user-coexist", SessionId);
// Add messages to both
var simpleContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Simple partitioning message")], []);
var hierarchicalContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Hierarchical partitioning message")], []);
var simpleContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Simple partitioning message")]);
var hierarchicalContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Hierarchical partitioning message")]);
await simpleProvider.InvokedAsync(simpleContext);
await hierarchicalProvider.InvokedAsync(hierarchicalContext);
@@ -763,7 +759,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await Task.Delay(10); // Small delay to ensure different timestamps
}
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages);
await provider.InvokedAsync(context);
// Wait for eventual consistency
@@ -801,7 +797,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
messages.Add(new ChatMessage(ChatRole.User, $"Message {i}"));
}
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages);
await provider.InvokedAsync(context);
// Wait for eventual consistency
@@ -56,7 +56,7 @@ public sealed class Mem0ProviderTests : IDisposable
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [input], aiContextProviderMessages: null));
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [input]));
var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
await sut.ClearStoredMemoriesAsync();
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]));
@@ -80,7 +80,7 @@ public sealed class Mem0ProviderTests : IDisposable
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [assistantIntro], aiContextProviderMessages: null));
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [assistantIntro]));
var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
await sut.ClearStoredMemoriesAsync();
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]));
@@ -108,7 +108,7 @@ public sealed class Mem0ProviderTests : IDisposable
Assert.DoesNotContain("Caoimhe", ctxBefore2.Messages?[0].Text ?? string.Empty);
// Act
await sut1.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [assistantIntro], aiContextProviderMessages: null));
await sut1.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [assistantIntro]));
var ctxAfterAdding1 = await GetContextWithRetryAsync(sut1, question);
var ctxAfterAdding2 = await GetContextWithRetryAsync(sut2, question);
@@ -218,7 +218,7 @@ public sealed class Mem0ProviderTests : IDisposable
};
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages) { ResponseMessages = responseMessages });
// Assert
var memoryPosts = this._handler.Requests.Where(r => r.RequestMessage.RequestUri!.AbsolutePath == "/v1/memories/" && r.RequestMessage.Method == HttpMethod.Post).ToList();
@@ -245,7 +245,7 @@ public sealed class Mem0ProviderTests : IDisposable
};
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null) { ResponseMessages = null, InvokeException = new InvalidOperationException("Request Failed") });
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages) { ResponseMessages = null, InvokeException = new InvalidOperationException("Request Failed") });
// Assert
Assert.Empty(this._handler.Requests);
@@ -271,7 +271,7 @@ public sealed class Mem0ProviderTests : IDisposable
};
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages) { ResponseMessages = responseMessages });
// Assert
this._loggerMock.Verify(
@@ -321,7 +321,7 @@ public sealed class Mem0ProviderTests : IDisposable
};
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages) { ResponseMessages = responseMessages });
// Assert
Assert.Equal(expectedLogCount, this._loggerMock.Invocations.Count);
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.UnitTests;
@@ -342,7 +343,8 @@ public partial class ChatClientAgentTests
var mockProvider = new Mock<AIContextProvider>();
mockProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new AIContext
{
Messages = aiContextProviderMessages,
@@ -350,7 +352,8 @@ public partial class ChatClientAgentTests
Tools = [AIFunctionFactory.Create(() => { }, "context provider function")]
});
mockProvider
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = (_, _) => new(mockProvider.Object), ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
@@ -378,12 +381,15 @@ public partial class ChatClientAgentTests
Assert.Equal("context provider message", chatHistoryProvider[1].Text);
Assert.Equal("response", chatHistoryProvider[2].Text);
mockProvider.Verify(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockProvider.Verify(p => p.InvokedAsync(It.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages == requestMessages &&
x.AIContextProviderMessages == aiContextProviderMessages &&
x.ResponseMessages == responseMessages &&
x.InvokeException == null), It.IsAny<CancellationToken>()), Times.Once);
mockProvider
.Protected()
.Verify<ValueTask<AIContext>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
mockProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages.Count() == requestMessages.Length + aiContextProviderMessages.Length &&
x.ResponseMessages == responseMessages &&
x.InvokeException == null), ItExpr.IsAny<CancellationToken>());
}
/// <summary>
@@ -394,7 +400,6 @@ public partial class ChatClientAgentTests
{
// Arrange
ChatMessage[] requestMessages = [new(ChatRole.User, "user message")];
ChatMessage[] responseMessages = [new(ChatRole.Assistant, "response")];
ChatMessage[] aiContextProviderMessages = [new(ChatRole.System, "context provider message")];
Mock<IChatClient> mockService = new();
mockService
@@ -406,13 +411,15 @@ public partial class ChatClientAgentTests
var mockProvider = new Mock<AIContextProvider>();
mockProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new AIContext
{
Messages = aiContextProviderMessages,
});
mockProvider
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = (_, _) => new(mockProvider.Object), ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
@@ -421,12 +428,15 @@ public partial class ChatClientAgentTests
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(requestMessages));
// Assert
mockProvider.Verify(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockProvider.Verify(p => p.InvokedAsync(It.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages == requestMessages &&
x.AIContextProviderMessages == aiContextProviderMessages &&
x.ResponseMessages == null &&
x.InvokeException is InvalidOperationException), It.IsAny<CancellationToken>()), Times.Once);
mockProvider
.Protected()
.Verify<ValueTask<AIContext>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
mockProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages.Count() == requestMessages.Length + aiContextProviderMessages.Length &&
x.ResponseMessages == null &&
x.InvokeException is InvalidOperationException), ItExpr.IsAny<CancellationToken>());
}
/// <summary>
@@ -458,7 +468,8 @@ public partial class ChatClientAgentTests
var mockProvider = new Mock<AIContextProvider>();
mockProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new AIContext());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = (_, _) => new(mockProvider.Object), ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
@@ -474,7 +485,9 @@ public partial class ChatClientAgentTests
Assert.Equal(ChatRole.User, capturedMessages[0].Role);
Assert.Single(capturedTools);
Assert.Contains(capturedTools, t => t.Name == "base function");
mockProvider.Verify(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockProvider
.Protected()
.Verify<ValueTask<AIContext>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
}
#endregion
@@ -1371,7 +1384,8 @@ public partial class ChatClientAgentTests
var mockProvider = new Mock<AIContextProvider>();
mockProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new AIContext
{
Messages = aiContextProviderMessages,
@@ -1379,7 +1393,8 @@ public partial class ChatClientAgentTests
Tools = [AIFunctionFactory.Create(() => { }, "context provider function")]
});
mockProvider
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
ChatClientAgent agent = new(
@@ -1414,13 +1429,16 @@ public partial class ChatClientAgentTests
Assert.Equal("context provider message", chatHistoryProvider[1].Text);
Assert.Equal("response", chatHistoryProvider[2].Text);
mockProvider.Verify(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockProvider.Verify(p => p.InvokedAsync(It.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages == requestMessages &&
x.AIContextProviderMessages == aiContextProviderMessages &&
x.ResponseMessages!.Count() == 1 &&
x.ResponseMessages!.ElementAt(0).Text == "response" &&
x.InvokeException == null), It.IsAny<CancellationToken>()), Times.Once);
mockProvider
.Protected()
.Verify<ValueTask<AIContext>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
mockProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages.Count() == requestMessages.Length + aiContextProviderMessages.Length &&
x.ResponseMessages!.Count() == 1 &&
x.ResponseMessages!.ElementAt(0).Text == "response" &&
x.InvokeException == null), ItExpr.IsAny<CancellationToken>());
}
/// <summary>
@@ -1442,13 +1460,15 @@ public partial class ChatClientAgentTests
var mockProvider = new Mock<AIContextProvider>();
mockProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new AIContext
{
Messages = aiContextProviderMessages,
});
mockProvider
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
ChatClientAgent agent = new(
@@ -1467,12 +1487,15 @@ public partial class ChatClientAgentTests
});
// Assert
mockProvider.Verify(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockProvider.Verify(p => p.InvokedAsync(It.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages == requestMessages &&
x.AIContextProviderMessages == aiContextProviderMessages &&
x.ResponseMessages == null &&
x.InvokeException is InvalidOperationException), It.IsAny<CancellationToken>()), Times.Once);
mockProvider
.Protected()
.Verify<ValueTask<AIContext>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
mockProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages.Count() == requestMessages.Length + aiContextProviderMessages.Length &&
x.ResponseMessages == null &&
x.InvokeException is InvalidOperationException), ItExpr.IsAny<CancellationToken>());
}
#endregion
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.UnitTests;
@@ -339,13 +340,15 @@ public class ChatClientAgent_BackgroundResponsesTests
// Create a mock chat history provider that would normally provide messages
var mockChatHistoryProvider = new Mock<ChatHistoryProvider>();
mockChatHistoryProvider
.Setup(ms => ms.InvokingAsync(It.IsAny<ChatHistoryProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync([new(ChatRole.User, "Message from chat history provider")]);
// Create a mock AI context provider that would normally provide context
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new AIContext
{
Messages = [new(ChatRole.System, "Message from AI context")],
@@ -385,14 +388,14 @@ public class ChatClientAgent_BackgroundResponsesTests
Assert.Empty(capturedMessages);
// Verify that chat history provider was never called due to continuation token
mockChatHistoryProvider.Verify(
ms => ms.InvokingAsync(It.IsAny<ChatHistoryProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
mockChatHistoryProvider
.Protected()
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Never(), ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
// Verify that AI context provider was never called due to continuation token
mockContextProvider.Verify(
p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
mockContextProvider
.Protected()
.Verify<ValueTask<AIContext>>("InvokingCoreAsync", Times.Never(), ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
}
[Fact]
@@ -404,13 +407,15 @@ public class ChatClientAgent_BackgroundResponsesTests
// Create a mock chat history provider that would normally provide messages
var mockChatHistoryProvider = new Mock<ChatHistoryProvider>();
mockChatHistoryProvider
.Setup(ms => ms.InvokingAsync(It.IsAny<ChatHistoryProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync([new(ChatRole.User, "Message from chat history provider")]);
// Create a mock AI context provider that would normally provide context
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new AIContext
{
Messages = [new(ChatRole.System, "Message from AI context")],
@@ -449,14 +454,14 @@ public class ChatClientAgent_BackgroundResponsesTests
Assert.Empty(capturedMessages);
// Verify that chat history provider was never called due to continuation token
mockChatHistoryProvider.Verify(
ms => ms.InvokingAsync(It.IsAny<ChatHistoryProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
mockChatHistoryProvider
.Protected()
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Never(), ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
// Verify that AI context provider was never called due to continuation token
mockContextProvider.Verify(
p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
mockContextProvider
.Protected()
.Verify<ValueTask<AIContext>>("InvokingCoreAsync", Times.Never(), ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
}
[Fact]
@@ -633,14 +638,16 @@ public class ChatClientAgent_BackgroundResponsesTests
List<ChatMessage> capturedMessagesAddedToProvider = [];
var mockChatHistoryProvider = new Mock<ChatHistoryProvider>();
mockChatHistoryProvider
.Setup(ms => ms.InvokedAsync(It.IsAny<ChatHistoryProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Callback<ChatHistoryProvider.InvokedContext, CancellationToken>((ctx, ct) => capturedMessagesAddedToProvider.AddRange(ctx.ResponseMessages ?? []))
.Returns(new ValueTask());
AIContextProvider.InvokedContext? capturedInvokedContext = null;
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Callback<AIContextProvider.InvokedContext, CancellationToken>((context, ct) => capturedInvokedContext = context)
.Returns(new ValueTask());
@@ -662,11 +669,15 @@ public class ChatClientAgent_BackgroundResponsesTests
await agent.RunStreamingAsync(session, options: runOptions).ToListAsync();
// Assert
mockChatHistoryProvider.Verify(ms => ms.InvokedAsync(It.IsAny<ChatHistoryProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockChatHistoryProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>());
Assert.Single(capturedMessagesAddedToProvider);
Assert.Contains("once upon a time", capturedMessagesAddedToProvider[0].Text);
mockContextProvider.Verify(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockContextProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>());
Assert.NotNull(capturedInvokedContext?.ResponseMessages);
Assert.Single(capturedInvokedContext.ResponseMessages);
Assert.Contains("once upon a time", capturedInvokedContext.ResponseMessages.ElementAt(0).Text);
@@ -689,14 +700,16 @@ public class ChatClientAgent_BackgroundResponsesTests
List<ChatMessage> capturedMessagesAddedToProvider = [];
var mockChatHistoryProvider = new Mock<ChatHistoryProvider>();
mockChatHistoryProvider
.Setup(ms => ms.InvokedAsync(It.IsAny<ChatHistoryProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Callback<ChatHistoryProvider.InvokedContext, CancellationToken>((ctx, ct) => capturedMessagesAddedToProvider.AddRange(ctx.RequestMessages))
.Returns(new ValueTask());
AIContextProvider.InvokedContext? capturedInvokedContext = null;
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Callback<AIContextProvider.InvokedContext, CancellationToken>((context, ct) => capturedInvokedContext = context)
.Returns(new ValueTask());
@@ -718,11 +731,15 @@ public class ChatClientAgent_BackgroundResponsesTests
await agent.RunStreamingAsync(session, options: runOptions).ToListAsync();
// Assert
mockChatHistoryProvider.Verify(ms => ms.InvokedAsync(It.IsAny<ChatHistoryProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockChatHistoryProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>());
Assert.Single(capturedMessagesAddedToProvider);
Assert.Contains("Tell me a story", capturedMessagesAddedToProvider[0].Text);
mockContextProvider.Verify(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
mockContextProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>());
Assert.NotNull(capturedInvokedContext?.RequestMessages);
Assert.Single(capturedInvokedContext.RequestMessages);
Assert.Contains("Tell me a story", capturedInvokedContext.RequestMessages.ElementAt(0).Text);
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Moq.Protected;
using Xunit.Sdk;
namespace Microsoft.Agents.AI.UnitTests;
@@ -183,12 +184,14 @@ public class ChatClientAgent_ChatHistoryManagementTests
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
Mock<ChatHistoryProvider> mockChatHistoryProvider = new();
mockChatHistoryProvider.Setup(s => s.InvokingAsync(
It.IsAny<ChatHistoryProvider.InvokingContext>(),
It.IsAny<CancellationToken>())).ReturnsAsync([new ChatMessage(ChatRole.User, "Existing Chat History")]);
mockChatHistoryProvider.Setup(s => s.InvokedAsync(
It.IsAny<ChatHistoryProvider.InvokedContext>(),
It.IsAny<CancellationToken>())).Returns(new ValueTask());
mockChatHistoryProvider
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync([new ChatMessage(ChatRole.User, "Existing Chat History")]);
mockChatHistoryProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
Mock<Func<ChatClientAgentOptions.ChatHistoryProviderFactoryContext, CancellationToken, ValueTask<ChatHistoryProvider>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatHistoryProviderFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockChatHistoryProvider.Object);
@@ -211,14 +214,16 @@ public class ChatClientAgent_ChatHistoryManagementTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
mockChatHistoryProvider.Verify(s => s.InvokingAsync(
It.Is<ChatHistoryProvider.InvokingContext>(x => x.RequestMessages.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockChatHistoryProvider.Verify(s => s.InvokedAsync(
It.Is<ChatHistoryProvider.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ChatHistoryProviderMessages != null && x.ChatHistoryProviderMessages.Count() == 1 && x.ResponseMessages!.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockChatHistoryProvider
.Protected()
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Once(),
ItExpr.Is<ChatHistoryProvider.InvokingContext>(x => x.RequestMessages.Count() == 1),
ItExpr.IsAny<CancellationToken>());
mockChatHistoryProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(),
ItExpr.Is<ChatHistoryProvider.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ResponseMessages!.Count() == 1),
ItExpr.IsAny<CancellationToken>());
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatHistoryProviderFactoryContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
@@ -253,10 +258,11 @@ public class ChatClientAgent_ChatHistoryManagementTests
// Assert
Assert.IsType<ChatHistoryProvider>(session!.ChatHistoryProvider, exactMatch: false);
mockChatHistoryProvider.Verify(s => s.InvokedAsync(
It.Is<ChatHistoryProvider.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ResponseMessages == null && x.InvokeException!.Message == "Test Error"),
It.IsAny<CancellationToken>()),
Times.Once);
mockChatHistoryProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(),
ItExpr.Is<ChatHistoryProvider.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ResponseMessages == null && x.InvokeException!.Message == "Test Error"),
ItExpr.IsAny<CancellationToken>());
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatHistoryProviderFactoryContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
@@ -308,22 +314,26 @@ public class ChatClientAgent_ChatHistoryManagementTests
// Arrange a chat history provider to override the factory provided one.
Mock<ChatHistoryProvider> mockOverrideChatHistoryProvider = new();
mockOverrideChatHistoryProvider.Setup(s => s.InvokingAsync(
It.IsAny<ChatHistoryProvider.InvokingContext>(),
It.IsAny<CancellationToken>())).ReturnsAsync([new ChatMessage(ChatRole.User, "Existing Chat History")]);
mockOverrideChatHistoryProvider.Setup(s => s.InvokedAsync(
It.IsAny<ChatHistoryProvider.InvokedContext>(),
It.IsAny<CancellationToken>())).Returns(new ValueTask());
mockOverrideChatHistoryProvider
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync([new ChatMessage(ChatRole.User, "Existing Chat History")]);
mockOverrideChatHistoryProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
// Arrange a chat history provider to provide to the agent via a factory at construction time.
// This one shouldn't be used since it is being overridden.
Mock<ChatHistoryProvider> mockFactoryChatHistoryProvider = new();
mockFactoryChatHistoryProvider.Setup(s => s.InvokingAsync(
It.IsAny<ChatHistoryProvider.InvokingContext>(),
It.IsAny<CancellationToken>())).ThrowsAsync(FailException.ForFailure("Base ChatHistoryProvider shouldn't be used."));
mockFactoryChatHistoryProvider.Setup(s => s.InvokedAsync(
It.IsAny<ChatHistoryProvider.InvokedContext>(),
It.IsAny<CancellationToken>())).Throws(FailException.ForFailure("Base ChatHistoryProvider shouldn't be used."));
mockFactoryChatHistoryProvider
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(FailException.ForFailure("Base ChatHistoryProvider shouldn't be used."));
mockFactoryChatHistoryProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Throws(FailException.ForFailure("Base ChatHistoryProvider shouldn't be used."));
Mock<Func<ChatClientAgentOptions.ChatHistoryProviderFactoryContext, CancellationToken, ValueTask<ChatHistoryProvider>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatHistoryProviderFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockFactoryChatHistoryProvider.Object);
@@ -348,23 +358,27 @@ public class ChatClientAgent_ChatHistoryManagementTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
mockOverrideChatHistoryProvider.Verify(s => s.InvokingAsync(
It.Is<ChatHistoryProvider.InvokingContext>(x => x.RequestMessages.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockOverrideChatHistoryProvider.Verify(s => s.InvokedAsync(
It.Is<ChatHistoryProvider.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ChatHistoryProviderMessages != null && x.ChatHistoryProviderMessages.Count() == 1 && x.ResponseMessages!.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockOverrideChatHistoryProvider
.Protected()
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Once(),
ItExpr.Is<ChatHistoryProvider.InvokingContext>(x => x.RequestMessages.Count() == 1),
ItExpr.IsAny<CancellationToken>());
mockOverrideChatHistoryProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(),
ItExpr.Is<ChatHistoryProvider.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ResponseMessages!.Count() == 1),
ItExpr.IsAny<CancellationToken>());
mockFactoryChatHistoryProvider.Verify(s => s.InvokingAsync(
It.IsAny<ChatHistoryProvider.InvokingContext>(),
It.IsAny<CancellationToken>()),
Times.Never);
mockFactoryChatHistoryProvider.Verify(s => s.InvokedAsync(
It.IsAny<ChatHistoryProvider.InvokedContext>(),
It.IsAny<CancellationToken>()),
Times.Never);
mockFactoryChatHistoryProvider
.Protected()
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Never(),
ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(),
ItExpr.IsAny<CancellationToken>());
mockFactoryChatHistoryProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Never(),
ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(),
ItExpr.IsAny<CancellationToken>());
}
#endregion
@@ -345,7 +345,7 @@ public sealed class TextSearchProviderTests
new ChatMessage(ChatRole.User, "C"),
new ChatMessage(ChatRole.Assistant, "D"),
};
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, initialMessages, aiContextProviderMessages: null) { InvokeException = new InvalidOperationException("Request Failed") });
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, initialMessages) { InvokeException = new InvalidOperationException("Request Failed") });
var invokingContext = new AIContextProvider.InvokingContext(
s_mockAgent,
@@ -387,7 +387,7 @@ public sealed class TextSearchProviderTests
new ChatMessage(ChatRole.User, "C"),
new ChatMessage(ChatRole.Assistant, "D"),
};
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, initialMessages, aiContextProviderMessages: null));
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, initialMessages));
var invokingContext = new AIContextProvider.InvokingContext(
s_mockAgent,
@@ -423,22 +423,22 @@ public sealed class TextSearchProviderTests
// First memory update (A,B)
await provider.InvokedAsync(new(
s_mockAgent,
s_mockSession,
[
new ChatMessage(ChatRole.User, "A"),
new ChatMessage(ChatRole.Assistant, "B"),
], aiContextProviderMessages: null));
s_mockAgent,
s_mockSession,
[
new ChatMessage(ChatRole.User, "A"),
new ChatMessage(ChatRole.Assistant, "B"),
]));
// Second memory update (C,D,E)
await provider.InvokedAsync(new(
s_mockAgent,
s_mockSession,
[
new ChatMessage(ChatRole.User, "C"),
new ChatMessage(ChatRole.Assistant, "D"),
new ChatMessage(ChatRole.User, "E"),
], aiContextProviderMessages: null));
s_mockAgent,
s_mockSession,
[
new ChatMessage(ChatRole.User, "C"),
new ChatMessage(ChatRole.Assistant, "D"),
new ChatMessage(ChatRole.User, "E"),
]));
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "F")]);
@@ -475,7 +475,7 @@ public sealed class TextSearchProviderTests
new ChatMessage(ChatRole.User, "U2"),
new ChatMessage(ChatRole.Assistant, "A2"),
};
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, initialMessages, null));
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, initialMessages));
var invokingContext = new AIContextProvider.InvokingContext(
s_mockAgent,
@@ -533,7 +533,7 @@ public sealed class TextSearchProviderTests
};
// Act
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, messages, aiContextProviderMessages: null)); // Populate recent memory.
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, messages)); // Populate recent memory.
var state = provider.Serialize();
// Assert
@@ -562,7 +562,7 @@ public sealed class TextSearchProviderTests
new ChatMessage(ChatRole.User, "C"),
new ChatMessage(ChatRole.Assistant, "D"),
};
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, messages, aiContextProviderMessages: null));
await provider.InvokedAsync(new(s_mockAgent, s_mockSession, messages));
// Act
var state = provider.Serialize();
@@ -603,7 +603,7 @@ public sealed class TextSearchProviderTests
new ChatMessage(ChatRole.Assistant, "L4"),
new ChatMessage(ChatRole.User, "L5"),
};
await initialProvider.InvokedAsync(new(s_mockAgent, s_mockSession, messages, aiContextProviderMessages: null));
await initialProvider.InvokedAsync(new(s_mockAgent, s_mockSession, messages));
var state = initialProvider.Serialize();
string? capturedInput = null;
@@ -119,7 +119,7 @@ public class ChatHistoryMemoryProviderTests
var requestMsgWithNulls = new ChatMessage(ChatRole.User, "request text nulls");
var responseMsg = new ChatMessage(ChatRole.Assistant, "response text") { MessageId = "resp-1", AuthorName = "assistant" };
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [requestMsgWithValues, requestMsgWithNulls], aiContextProviderMessages: null)
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [requestMsgWithValues, requestMsgWithNulls])
{
ResponseMessages = [responseMsg]
};
@@ -177,7 +177,7 @@ public class ChatHistoryMemoryProviderTests
1,
new ChatHistoryMemoryProviderScope() { UserId = "UID" });
var requestMsg = new ChatMessage(ChatRole.User, "request text") { MessageId = "req-1" };
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [requestMsg], aiContextProviderMessages: null)
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [requestMsg])
{
InvokeException = new InvalidOperationException("Invoke failed")
};
@@ -206,7 +206,7 @@ public class ChatHistoryMemoryProviderTests
new ChatHistoryMemoryProviderScope() { UserId = "UID" },
loggerFactory: this._loggerFactoryMock.Object);
var requestMsg = new ChatMessage(ChatRole.User, "request text") { MessageId = "req-1" };
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [requestMsg], aiContextProviderMessages: null);
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [requestMsg]);
// Act
await provider.InvokedAsync(invokedContext, CancellationToken.None);
@@ -257,7 +257,7 @@ public class ChatHistoryMemoryProviderTests
loggerFactory: this._loggerFactoryMock.Object);
var requestMsg = new ChatMessage(ChatRole.User, "request text");
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [requestMsg], aiContextProviderMessages: null);
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [requestMsg]);
// Act
await provider.InvokedAsync(invokedContext, CancellationToken.None);