// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; /// /// Provides an abstract base class for components that enhance AI context during agent invocations. /// /// /// /// An AI context provider is a component that participates in the agent invocation lifecycle by: /// /// Listening to changes in conversations /// Providing additional context to agents during invocation /// Supplying additional function tools for enhanced capabilities /// Processing invocation results for state management or learning /// /// /// /// Context providers operate through a two-phase lifecycle: they are called at the start of invocation via /// to provide context, and optionally called at the end of invocation via /// to process results. /// /// /// Security considerations: Context providers may inject messages with any role, including system, which /// has the highest trust level and directly shapes LLM behavior. Developers must ensure that all providers attached to an agent /// are trusted. Agent Framework does not validate or filter the data returned by providers — it is accepted as-is and merged into /// the request context. If a provider retrieves data from an external source (e.g., a vector database or memory service), be aware /// that a compromised data source could introduce adversarial content designed to manipulate LLM behavior via indirect prompt injection. /// Implementers should validate and sanitize data retrieved from external sources before returning it. /// /// public abstract class AIContextProvider { private static IEnumerable DefaultExternalOnlyFilter(IEnumerable messages) => messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External); private static IEnumerable DefaultNoopFilter(IEnumerable messages) => messages; private IReadOnlyList? _stateKeys; /// /// Initializes a new instance of the class. /// /// An optional filter function to apply to input messages before providing context via . If not set, defaults to including only messages. /// An optional filter function to apply to request messages before storing context via . If not set, defaults to including only messages. /// An optional filter function to apply to response messages before storing context via . If not set, defaults to a no-op filter that includes all response messages. protected AIContextProvider( Func, IEnumerable>? provideInputMessageFilter = null, Func, IEnumerable>? storeInputRequestMessageFilter = null, Func, IEnumerable>? storeInputResponseMessageFilter = null) { this.ProvideInputMessageFilter = provideInputMessageFilter ?? DefaultExternalOnlyFilter; this.StoreInputRequestMessageFilter = storeInputRequestMessageFilter ?? DefaultExternalOnlyFilter; this.StoreInputResponseMessageFilter = storeInputResponseMessageFilter ?? DefaultNoopFilter; } /// /// Gets the filter function to apply to input messages before providing context via . /// protected Func, IEnumerable> ProvideInputMessageFilter { get; } /// /// Gets the filter function to apply to request messages before storing context via . /// protected Func, IEnumerable> StoreInputRequestMessageFilter { get; } /// /// Gets the filter function to apply to response messages before storing context via . /// protected Func, IEnumerable> StoreInputResponseMessageFilter { get; } /// /// Gets the set of keys used to store the provider state in the . /// /// /// The default value is a single-element set containing the name of the concrete type (e.g. "TextSearchProvider"). /// Implementations may override this to provide custom keys, for example when multiple /// instances of the same provider type are used in the same session, or when a provider /// stores state under more than one key. /// public virtual IReadOnlyList StateKeys => this._stateKeys ??= [this.GetType().Name]; /// /// Called at the start of agent invocation to provide additional context. /// /// Contains the request context including the caller provided messages that will be used by the agent for this invocation. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. The task result contains the with additional context to be used by the agent during this invocation. /// /// /// Implementers can load any additional context required at this time, such as: /// /// Retrieving relevant information from knowledge bases /// Adding system instructions or prompts /// Providing function tools for the current invocation /// Injecting contextual messages from conversation history /// /// /// /// Security consideration: Data retrieved from external sources (e.g., vector databases, memory services, or /// knowledge bases) may contain adversarial content designed to influence LLM behavior via indirect prompt injection. /// Implementers should validate data integrity and consider the trustworthiness of the data source. /// /// public ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) => this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken); /// /// Called at the start of agent invocation to provide additional context. /// /// Contains the request context including the caller provided messages that will be used by the agent for this invocation. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. The task result contains the with additional context to be used by the agent during this invocation. /// /// /// Implementers can load any additional context required at this time, such as: /// /// Retrieving relevant information from knowledge bases /// Adding system instructions or prompts /// Providing function tools for the current invocation /// Injecting contextual messages from conversation history /// /// /// /// The default implementation of this method filters the input messages using the configured provide-input message filter /// (which defaults to including only messages), /// then calls to get additional context, /// stamps any messages from the returned context with source attribution, /// and merges the returned context with the original (unfiltered) input context (concatenating instructions, messages, and tools). /// For most scenarios, overriding is sufficient to provide additional context, /// while still benefiting from the default filtering, merging and source stamping behavior. /// However, for scenarios that require more control over context filtering, merging or source stamping, overriding this method /// allows you to directly control the full returned for the invocation. /// /// protected virtual async ValueTask InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default) { var inputContext = context.AIContext; // Create a filtered context for ProvideAIContextAsync, filtering input messages // to exclude non-external messages (e.g. chat history, other AI context provider messages). #pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var filteredContext = new InvokingContext( context.Agent, context.Session, new AIContext { Instructions = inputContext.Instructions, Messages = inputContext.Messages is not null ? this.ProvideInputMessageFilter(inputContext.Messages) : null, Tools = inputContext.Tools }); #pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var provided = await this.ProvideAIContextAsync(filteredContext, cancellationToken).ConfigureAwait(false); var mergedInstructions = (inputContext.Instructions, provided.Instructions) switch { (null, null) => null, (string a, null) => a, (null, string b) => b, (string a, string b) => a + "\n" + b }; var providedMessages = provided.Messages is not null ? provided.Messages.Select(m => m.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!)) : null; var mergedMessages = (inputContext.Messages, providedMessages) switch { (null, null) => null, (var a, null) => a, (null, var b) => b, (var a, var b) => a.Concat(b) }; var mergedTools = (inputContext.Tools, provided.Tools) switch { (null, null) => null, (var a, null) => a, (null, var b) => b, (var a, var b) => a.Concat(b) }; return new AIContext { Instructions = mergedInstructions, Messages = mergedMessages, Tools = mergedTools }; } /// /// When overridden in a derived class, provides additional AI context to be merged with the input context for the current invocation. /// /// /// /// This method is called from . /// Note that can be overridden to directly control context merging and source stamping, in which case /// it is up to the implementer to call this method as needed to retrieve the additional context. /// /// /// In contrast with , this method only returns additional context to be merged with the input, /// while is responsible for returning the full merged for the invocation. /// /// /// Security consideration: Any messages, tools, or instructions returned by this method will be merged into the /// AI request context. If data is retrieved from external or untrusted sources, implementers should validate and sanitize it /// to prevent indirect prompt injection attacks. /// /// /// Contains the request context including the caller provided messages that will be used by the agent for this invocation. /// The to monitor for cancellation requests. The default is . /// /// A task that represents the asynchronous operation. The task result contains an /// with additional context to be merged with the input context. /// protected virtual ValueTask ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default) { return new ValueTask(new AIContext()); } /// /// Called at the end of the agent invocation to process the invocation results. /// /// Contains the invocation context including request messages, response messages, and any exception that occurred. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. /// /// /// Implementers can use the request and response messages in the provided to: /// /// Update state based on conversation outcomes /// Extract and store memories or preferences from user messages /// Log or audit conversation details /// Perform cleanup or finalization tasks /// /// /// /// The is passed a reference to the via and /// allowing it to store state in the . Since an is used with many different sessions, it should /// not store any session-specific information within its own instance fields. Instead, any session-specific state should be stored in the associated . /// /// /// This method is called regardless of whether the invocation succeeded or failed. /// To check if the invocation was successful, inspect the property. /// /// public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) => this.InvokedCoreAsync(Throw.IfNull(context), cancellationToken); /// /// Called at the end of the agent invocation to process the invocation results. /// /// Contains the invocation context including request messages, response messages, and any exception that occurred. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. /// /// /// Implementers can use the request and response messages in the provided to: /// /// Update internal state based on conversation outcomes /// Extract and store memories or preferences from user messages /// Log or audit conversation details /// Perform cleanup or finalization tasks /// /// /// /// This method is called regardless of whether the invocation succeeded or failed. /// To check if the invocation was successful, inspect the property. /// /// /// The default implementation of this method skips execution for any invocation failures, /// filters the request messages using the configured store-input request message filter /// (which defaults to including only messages), /// filters the response messages using the configured store-input response message filter /// (which defaults to a no-op, so all response messages are processed), /// and calls to process the invocation results. /// For most scenarios, overriding is sufficient to process invocation results, /// while still benefiting from the default error handling and filtering behavior. /// However, for scenarios that require more control over error handling or message filtering, overriding this method /// allows you to directly control the processing of invocation results. /// /// protected virtual ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default) { if (context.InvokeException is not null) { return default; } #pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var subContext = new InvokedContext(context.Agent, context.Session, this.StoreInputRequestMessageFilter(context.RequestMessages), this.StoreInputResponseMessageFilter(context.ResponseMessages!)); #pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return this.StoreAIContextAsync(subContext, cancellationToken); } /// /// When overridden in a derived class, processes invocation results at the end of the agent invocation. /// /// Contains the invocation context including request messages, response messages, and any exception that occurred. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. /// /// /// This method is called from . /// Note that can be overridden to directly control error handling, in which case /// it is up to the implementer to call this method as needed to process the invocation results. /// /// /// In contrast with , this method only processes the invocation results, /// while is also responsible for error handling. /// /// /// The default implementation of only calls this method if the invocation succeeded. /// /// /// Security consideration: Messages being processed/stored may contain PII and sensitive conversation content. /// Implementers should ensure appropriate encryption at rest and access controls for the storage backend. /// /// protected virtual ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default) => default; /// Asks the for an object of the specified type . /// The type of object being requested. /// An optional key that can be used to help identify the target service. /// The found object, otherwise . /// is . /// /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , /// including itself or any services it might be wrapping. This enables advanced scenarios where consumers need access to /// specific provider implementations or their internal services. /// public virtual object? GetService(Type serviceType, object? serviceKey = null) { _ = Throw.IfNull(serviceType); return serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; } /// Asks the for an object of type . /// The type of the object to be retrieved. /// An optional key that can be used to help identify the target service. /// The found object, otherwise . /// /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , /// including itself or any services it might be wrapping. This is a convenience overload of . /// public TService? GetService(object? serviceKey = null) => this.GetService(typeof(TService), serviceKey) is TService service ? service : default; /// /// Contains the context information provided to . /// /// /// This class provides context about the invocation before the underlying AI model is invoked, including the messages /// that will be used. Context providers can use this information to determine what additional context /// should be provided for the invocation. /// public sealed class InvokingContext { /// /// Initializes a new instance of the class. /// /// The agent being invoked. /// The session associated with the agent invocation. /// The AI context to be used by the agent for this invocation. /// or is . [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokingContext( AIAgent agent, AgentSession? session, AIContext aiContext) { this.Agent = Throw.IfNull(agent); this.Session = session; this.AIContext = Throw.IfNull(aiContext); } /// /// Gets the agent that is being invoked. /// public AIAgent Agent { get; } /// /// Gets the agent session associated with the agent invocation. /// public AgentSession? Session { get; } /// /// Gets the being built for the current invocation. Context providers can modify /// and return or return a new instance to provide additional context for the invocation. /// /// /// /// If multiple instances are used in the same invocation, each /// will receive the context returned by the previous allowing them to build on top of each other's context. /// /// /// The first in the invocation pipeline will receive an instance /// that already contains the caller provided messages that will be used by the agent for this invocation. /// /// /// It may also contain messages from chat history, if a is being used. /// /// public AIContext AIContext { get; } } /// /// Contains the context information provided to . /// /// /// This class provides context about a completed agent invocation, including the accumulated /// request messages (user input, chat history and any others provided by AI context providers) that were used /// and the response messages that were generated. It also indicates whether the invocation succeeded or failed. /// public sealed class InvokedContext { /// /// Initializes a new instance of the class for a successful invocation. /// /// The agent that was invoked. /// The session associated with the agent invocation. /// The accumulated request messages (user input, chat history and any others provided by AI context providers) /// that were used by the agent for this invocation. /// The response messages generated during this invocation. /// , , or is . [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokedContext( AIAgent agent, AgentSession? session, IEnumerable requestMessages, IEnumerable responseMessages) { this.Agent = Throw.IfNull(agent); this.Session = session; this.RequestMessages = Throw.IfNull(requestMessages); this.ResponseMessages = Throw.IfNull(responseMessages); } /// /// Initializes a new instance of the class for a failed invocation. /// /// The agent that was invoked. /// The session associated with the agent invocation. /// The accumulated request messages (user input, chat history and any others provided by AI context providers) /// that were used by the agent for this invocation. /// The exception that caused the invocation to fail. /// , , or is . public InvokedContext( AIAgent agent, AgentSession? session, IEnumerable requestMessages, Exception invokeException) { this.Agent = Throw.IfNull(agent); this.Session = session; this.RequestMessages = Throw.IfNull(requestMessages); this.InvokeException = Throw.IfNull(invokeException); } /// /// Gets the agent that is being invoked. /// public AIAgent Agent { get; } /// /// Gets the agent session associated with the agent invocation. /// public AgentSession? Session { get; } /// /// Gets the accumulated request messages (user input, chat history and any others provided by AI context providers) /// that were used by the agent for this invocation. /// /// /// A collection of instances representing all messages that were used by the agent for this invocation. /// public IEnumerable RequestMessages { get; } /// /// Gets the collection of response messages generated during this invocation if the invocation succeeded. /// /// /// A collection of instances representing the response, /// or if the invocation failed. /// public IEnumerable? ResponseMessages { get; } /// /// Gets the that was thrown during the invocation, if the invocation failed. /// /// /// The exception that caused the invocation to fail, or if the invocation succeeded. /// public Exception? InvokeException { get; } } }