Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,19 @@ protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessag
}

// Get existing messages from the store
var invokingContext = new ChatMessageStore.InvokingContext(messages);
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken);
var invokingContext = new AIContextProvider.InvokingContext(messages);
var historyProviderAIContext = await typedThread.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken);

// Clone the input messages and turn them into response messages with upper case text.
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();

// Notify the thread of the input and output messages.
var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages)
var invokedContext = new AIContextProvider.InvokedContext(messages, null)
{
ChatHistoryMessages = historyProviderAIContext.Messages,
ResponseMessages = responseMessages
};
await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken);
await typedThread.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);

return new AgentResponse
{
Expand All @@ -77,18 +78,19 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
}

// Get existing messages from the store
var invokingContext = new ChatMessageStore.InvokingContext(messages);
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken);
var invokingContext = new AIContextProvider.InvokingContext(messages);
var historyProviderAIContext = await typedThread.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken);

// Clone the input messages and turn them into response messages with upper case text.
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();

// Notify the thread of the input and output messages.
var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages)
var invokedContext = new AIContextProvider.InvokedContext(messages, null)
{
ChatHistoryMessages = historyProviderAIContext.Messages,
ResponseMessages = responseMessages
};
await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken);
await typedThread.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);

foreach (var message in responseMessages)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
// Since we are using ChatCompletion which stores chat history locally, we can also add a message removal policy
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
// we don't bloat chat history with all the search result messages.
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(new InMemoryChatMessageStore(ctx.SerializedState, ctx.JsonSerializerOptions)
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new InMemoryChatHistoryProvider(ctx.SerializedState, ctx.JsonSerializerOptions)
.WithAIContextProviderMessageRemoval()),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(
// Create a new chat message store for this agent that stores the messages in a vector store.
// Each thread must get its own copy of the VectorChatMessageStore, since the store
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(
// Create a new chat history provider for this agent that stores the chat history in a vector store.
// Each thread must get its own copy of the VectorStoreChatHistoryProvider, since the store
// also contains the id that the thread is stored under.
new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions))
new VectorStoreChatHistoryProvider(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions))
});

// Start a new thread for the agent conversation.
Expand All @@ -61,20 +61,20 @@
// Run the agent with the thread that stores conversation history in the vector store a second time.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));

// We can access the VectorChatMessageStore via the thread's GetService method if we need to read the key under which threads are stored.
var messageStore = resumedThread.GetService<VectorChatMessageStore>()!;
Console.WriteLine($"\nThread is stored in vector store under key: {messageStore.ThreadDbKey}");
// We can access the VectorStoreChatHistoryProvider via the thread's GetService method if we need to read the key under which threads are stored.
var chatHistoryProvider = resumedThread.GetService<VectorStoreChatHistoryProvider>()!;
Console.WriteLine($"\nThread is stored in vector store under key: {chatHistoryProvider.ThreadDbKey}");

namespace SampleApp
{
/// <summary>
/// A sample implementation of <see cref="ChatMessageStore"/> that stores chat messages in a vector store.
/// A sample implementation of <see cref="AIContextProvider"/> that stores and provides chat history from a vector store.
/// </summary>
internal sealed class VectorChatMessageStore : ChatMessageStore
internal sealed class VectorStoreChatHistoryProvider : AIContextProvider
{
private readonly VectorStore _vectorStore;

public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
public VectorStoreChatHistoryProvider(VectorStore vectorStore, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
{
this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));

Expand All @@ -87,7 +87,7 @@ public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedSto

public string? ThreadDbKey { get; private set; }

public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
public override async ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
Expand All @@ -102,7 +102,7 @@ public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(Invoking
var messages = records.ConvertAll(x => JsonSerializer.Deserialize<ChatMessage>(x.SerializedMessage!)!)
;
messages.Reverse();
return messages;
return new() { Messages = messages };
}

public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";

// Construct the agent, and provide a factory to create an in-memory chat message store with a reducer that keeps only the last 2 non-system messages.
// Construct the agent, and provide a factory to create an in-memory chat history provider with a reducer that keeps only the last 2 non-system messages.
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
Expand All @@ -24,7 +24,7 @@
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions))
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new InMemoryChatHistoryProvider(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions))
});

AgentThread thread = await agent.GetNewThreadAsync();
Expand Down
13 changes: 13 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,19 @@ public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<Chat
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }

/// <summary>
/// Gets the messages retrieved from the chat history provider for this invocation, if any.
/// </summary>
/// <remarks>
/// Note that if chat history is stored in the underlying AI service, this property will be null.
/// Only chat history retrieved via a chat history provider will be provided here.
/// </remarks>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances that were retrieved from the chat history,
/// and were used by the agent as part of the invocation.
/// </value>
public IEnumerable<ChatMessage>? ChatHistoryMessages { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between this and AIContextProviderMessages? If the idea is that a chat message store is now a context provider, then isn't such a context provider's messages the history?


/// <summary>
/// Gets the messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.

using System;

namespace Microsoft.Agents.AI;

/// <summary>
/// Contains extension methods for the <see cref="AIContextProvider"/> class.
/// </summary>
public static class AIContextProviderExtensions
{
/// <summary>
/// Adds message filtering to an existing <see cref="AIContextProvider"/>, so that data passed to to and from it
/// can be filtered, updated or replaced.
/// </summary>
/// <param name="innerAIContextProvider">The underlying AI context provider to be wrapped. Cannot be null.</param>
/// <param name="invokingContextFilter">An optional filter function to apply to the AI context before it is returned. If null, no filter is applied at this
/// stage.</param>
/// <param name="invokedContextFilter">An optional filter function to apply to the invocation context before it is consumed. If null, no
/// filter is applied at this stage.</param>
/// <returns>The <see cref="AIContextProvider"/> with filtering applied.</returns>
public static AIContextProvider WithMessageFilters(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this effectively creating a pipeline of providers as decorators?

this AIContextProvider innerAIContextProvider,
Func<AIContext, AIContext>? invokingContextFilter = null,
Func<AIContextProvider.InvokedContext, AIContextProvider.InvokedContext>? invokedContextFilter = null)
{
return new MessageFilteringAIContextProvider(
innerAIContextProvider,
invokingContextFilter,
invokedContextFilter);
}

/// <summary>
/// Decorates the provided <see cref="AIContextProvider"/> so that it does not receive messages produced by any <see cref="AIContextProvider"/>.
/// </summary>
/// <param name="innerAIContextProvider">The underlying AI context provider to add the filter to. Cannot be null.</param>
/// <returns>A new <see cref="AIContextProvider"/> instance that filters out <see cref="AIContextProvider"/> messages.</returns>
public static AIContextProvider WithAIContextProviderMessageRemoval(this AIContextProvider innerAIContextProvider)
{
return new MessageFilteringAIContextProvider(
innerAIContextProvider,
invokedContextFilter: (ctx) =>
{
ctx.AIContextProviderMessages = null;
return ctx;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(AgentResponseUpdate[]))]
[JsonSerializable(typeof(ServiceIdAgentThread.ServiceIdAgentThreadState))]
[JsonSerializable(typeof(InMemoryAgentThread.InMemoryAgentThreadState))]
[JsonSerializable(typeof(InMemoryChatMessageStore.StoreState))]
[JsonSerializable(typeof(InMemoryChatHistoryProvider.State))]

[ExcludeFromCodeCoverage]
private sealed partial class JsonContext : JsonSerializerContext;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOption
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AgentThread"/>,
/// including itself or any services it might be wrapping. For example, to access a <see cref="ChatMessageStore"/> if available for the instance,
/// including itself or any services it might be wrapping. For example, to access an <see cref="AIContextProvider"/> if available for the instance,
/// <see cref="GetService"/> may be used to request it.
/// </remarks>
public virtual object? GetService(Type serviceType, object? serviceKey = null)
Expand Down
Loading
Loading