.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
@@ -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) &&