Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -2,8 +2,9 @@

// This sample shows multiple middleware layers working together with Azure OpenAI:
// chat client (global/per-request), agent run (PII filtering and guardrails),
// function invocation (logging and result overrides), and human-in-the-loop
// approval workflows for sensitive function calls.
// function invocation (logging and result overrides), human-in-the-loop
// approval workflows for sensitive function calls, and MessageAIContextProvider
// middleware for injecting additional context messages into the agent pipeline.

using System.ComponentModel;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -96,6 +97,20 @@ static string GetDateTime()

Console.WriteLine($"Per-request middleware response: {response}");

// MessageAIContextProvider middleware that injects additional messages into the agent request.
// This allows any AIAgent (not just ChatClientAgent) to benefit from MessageAIContextProvider-based
// context enrichment. Multiple providers can be passed to Use and they are called in sequence,
// each receiving the output of the previous one.
Console.WriteLine("\n\n=== Example 5: MessageAIContextProvider middleware ===");

var contextProviderAgent = originalAgent
.AsBuilder()
.Use([new DateTimeContextProvider()])
.Build();

var contextResponse = await contextProviderAgent.RunAsync("Is it almost time for lunch?");
Console.WriteLine($"Context-enriched response: {contextResponse}");

// Function invocation middleware that logs before and after function calls.
async ValueTask<object?> FunctionCallMiddleware(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -259,3 +274,23 @@ async Task<ChatResponse> PerRequestChatClientMiddleware(IEnumerable<ChatMessage>

return response;
}

/// <summary>
/// A <see cref="MessageAIContextProvider"/> that injects the current date and time into the agent's context.
/// This is a simple example of how to use a MessageAIContextProvider to enrich agent messages
/// via the <see cref="AIAgentBuilder.Use(MessageAIContextProvider[])"/> extension method.
/// </summary>
internal sealed class DateTimeContextProvider : MessageAIContextProvider
{
protected override ValueTask<IEnumerable<ChatMessage>> ProvideMessagesAsync(
InvokingContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine("DateTimeContextProvider - Injecting current date/time context");

return new ValueTask<IEnumerable<ChatMessage>>(
[
new ChatMessage(ChatRole.User, $"For reference, the current date and time is: {DateTimeOffset.Now}")
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This sample demonstrates how to add middleware to intercept:
5. Per‑request chat client middleware
6. Per‑request function pipeline with approval
7. Combining agent‑level and per‑request middleware
8. MessageAIContextProvider middleware via `AIAgentBuilder.Use(...)` for injecting additional context messages

## Function Invocation Middleware

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,12 @@ private static void AddTodoItem(AgentSession? session, string item)
}

/// <summary>
/// An <see cref="AIContextProvider"/> which searches for upcoming calendar events and adds them to the AI context.
/// A <see cref="MessageAIContextProvider"/> which searches for upcoming calendar events and adds them to the AI context.
/// </summary>
internal sealed class CalendarSearchAIContextProvider(Func<Task<string[]>> loadNextThreeCalendarEvents) : AIContextProvider
internal sealed class CalendarSearchAIContextProvider(Func<Task<string[]>> loadNextThreeCalendarEvents) : MessageAIContextProvider
{
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
protected override async ValueTask<IEnumerable<MEAI.ChatMessage>> ProvideMessagesAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var inputContext = context.AIContext;
var events = await loadNextThreeCalendarEvents();

StringBuilder outputMessageBuilder = new();
Expand All @@ -166,18 +165,7 @@ protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext
outputMessageBuilder.AppendLine($" - {calendarEvent}");
}

return new()
{
Instructions = inputContext.Instructions,
Messages =
(inputContext.Messages ?? [])
.Concat(
[
new MEAI.ChatMessage(ChatRole.User, outputMessageBuilder.ToString()).WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!)
])
.ToList(),
Tools = inputContext.Tools
};
return [new MEAI.ChatMessage(ChatRole.User, outputMessageBuilder.ToString())];
}
}
}
21 changes: 14 additions & 7 deletions dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ public abstract class AIContextProvider
private static IEnumerable<ChatMessage> DefaultExternalOnlyFilter(IEnumerable<ChatMessage> messages)
=> messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External);

private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _provideInputMessageFilter;
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _storeInputMessageFilter;

/// <summary>
/// Initializes a new instance of the <see cref="AIContextProvider"/> class.
/// </summary>
Expand All @@ -46,10 +43,20 @@ protected AIContextProvider(
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputMessageFilter = null)
{
this._provideInputMessageFilter = provideInputMessageFilter ?? DefaultExternalOnlyFilter;
this._storeInputMessageFilter = storeInputMessageFilter ?? DefaultExternalOnlyFilter;
this.ProvideInputMessageFilter = provideInputMessageFilter ?? DefaultExternalOnlyFilter;
this.StoreInputMessageFilter = storeInputMessageFilter ?? DefaultExternalOnlyFilter;
}

/// <summary>
/// Gets the filter function to apply to input messages before providing context via <see cref="ProvideAIContextAsync"/>.
/// </summary>
protected Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> ProvideInputMessageFilter { get; }

/// <summary>
/// Gets the filter function to apply to request messages before storing context via <see cref="StoreAIContextAsync"/>.
/// </summary>
protected Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> StoreInputMessageFilter { get; }

/// <summary>
/// Gets the key used to store the provider state in the <see cref="AgentSession.StateBag"/>.
/// </summary>
Expand Down Expand Up @@ -120,7 +127,7 @@ protected virtual async ValueTask<AIContext> InvokingCoreAsync(InvokingContext c
new AIContext
{
Instructions = inputContext.Instructions,
Messages = inputContext.Messages is not null ? this._provideInputMessageFilter(inputContext.Messages) : null,
Messages = inputContext.Messages is not null ? this.ProvideInputMessageFilter(inputContext.Messages) : null,
Tools = inputContext.Tools
});

Expand Down Expand Up @@ -254,7 +261,7 @@ protected virtual ValueTask InvokedCoreAsync(InvokedContext context, Cancellatio
return default;
}

var subContext = new InvokedContext(context.Agent, context.Session, this._storeInputMessageFilter(context.RequestMessages), context.ResponseMessages!);
var subContext = new InvokedContext(context.Agent, context.Session, this.StoreInputMessageFilter(context.RequestMessages), context.ResponseMessages!);
return this.StoreAIContextAsync(subContext, cancellationToken);
}

Expand Down
Loading
Loading