Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessag
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();

// Notify the session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(messages)
{
ResponseMessages = responseMessages
};
Expand Down Expand Up @@ -84,7 +84,7 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();

// Notify the session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(messages)
{
ResponseMessages = responseMessages
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonS

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))
Expand All @@ -122,7 +122,7 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio
}
}

public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
StringBuilder instructions = new();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public VectorChatHistoryProvider(VectorStore vectorStore, JsonElement serialized

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);
Expand All @@ -105,7 +105,7 @@ public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(Invoking
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)
Expand All @@ -120,7 +120,7 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio

// 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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public TodoListAIContextProvider(JsonElement jsonElement, JsonSerializerOptions?
}
}

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:");
Expand Down Expand Up @@ -132,7 +132,7 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio
/// </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();

Expand Down Expand Up @@ -179,7 +179,7 @@ public AggregatingAIContextProvider(ProviderFactory[] providerFactories, JsonEle
.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());
Expand Down
90 changes: 74 additions & 16 deletions dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -48,7 +49,51 @@ 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
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out var source)
&& source is AgentRequestMessageSource typedSource
&& typedSource == AgentRequestMessageSource.AIContextProvider)
{
Comment thread
westey-m marked this conversation as resolved.
return message;
}

message = message.Clone();
message.AdditionalProperties ??= new();
message.AdditionalProperties[AgentRequestMessageSource.AdditionalPropertiesKey] = AgentRequestMessageSource.AIContextProvider;
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);
Comment thread
westey-m marked this conversation as resolved.

/// <summary>
/// Called at the end of the agent invocation to process the invocation results.
Expand All @@ -71,7 +116,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>
Expand Down Expand Up @@ -117,7 +186,7 @@ public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOption
=> 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
Expand Down Expand Up @@ -146,7 +215,7 @@ public InvokingContext(IEnumerable<ChatMessage> requestMessages)
}

/// <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
Expand All @@ -159,12 +228,10 @@ public sealed class InvokedContext
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
/// </summary>
/// <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(IEnumerable<ChatMessage> requestMessages, IEnumerable<ChatMessage>? aiContextProviderMessages)
public InvokedContext(IEnumerable<ChatMessage> requestMessages)
{
this.RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
this.AIContextProviderMessages = aiContextProviderMessages;
}

/// <summary>
Expand All @@ -176,15 +243,6 @@ public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<Chat
/// </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>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.Extensions.AI;
Comment thread
westey-m marked this conversation as resolved.
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;

/// <summary>
/// An enumeration representing the source of an agent request message.
/// </summary>
/// <remarks>
/// Input messages for a specific agent run can originate from various sources.
/// This enumeration helps to identify whether a message came from outside the agent pipeline,
Comment thread
westey-m marked this conversation as resolved.
Outdated
/// whether it was produced by middleware, or came from chat history.
/// </remarks>
public sealed class AgentRequestMessageSource : IEquatable<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";

/// <summary>
/// Initializes a new instance of the <see cref="AgentRequestMessageSource"/> class.
/// </summary>
/// <param name="value">The string value representing the source of the agent request message.</param>
public AgentRequestMessageSource(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 AgentRequestMessageSource External { get; } = new AgentRequestMessageSource(nameof(External));

/// <summary>
/// The message was produced by middleware.
/// </summary>
public static AgentRequestMessageSource AIContextProvider { get; } = new AgentRequestMessageSource(nameof(AIContextProvider));

/// <summary>
/// The message came from chat history.
/// </summary>
public static AgentRequestMessageSource ChatHistory { get; } = new AgentRequestMessageSource(nameof(ChatHistory));

/// <summary>
/// Determines whether this instance and another specified <see cref="AgentRequestMessageSource"/> object have the same value.
/// </summary>
/// <param name="other">The <see cref="AgentRequestMessageSource"/> 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(AgentRequestMessageSource? 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="AgentRequestMessageSource"/> and its value is the same as this instance; otherwise, <see langword="false"/>.</returns>
public override bool Equals(object? obj) => this.Equals(obj as AgentRequestMessageSource);

/// <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="AgentRequestMessageSource"/> objects have the same value.
/// </summary>
/// <param name="left">The first <see cref="AgentRequestMessageSource"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSource"/> 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 ==(AgentRequestMessageSource? left, AgentRequestMessageSource? right)
{
if (left is null)
{
return right is null;
}

return left.Equals(right);
}

/// <summary>
/// Determines whether two specified <see cref="AgentRequestMessageSource"/> objects have different values.
/// </summary>
/// <param name="left">The first <see cref="AgentRequestMessageSource"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSource"/> 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 !=(AgentRequestMessageSource? left, AgentRequestMessageSource? right) => !(left == right);
}
Loading
Loading