Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -8,6 +8,11 @@
// even if the process is interrupted mid-loop, but may also result in chat history that is not
// yet finalized (e.g., tool calls without results) being persisted, which may be undesirable in some cases.
//
// Additionally, this sample demonstrates the IChatMessageInjector feature, which allows tool
// code to inject new user messages during the function execution loop. When a tool or anything else enqueues
// a message via IChatMessageInjector.EnqueueMessages during the tool execution loop, the PerServiceCallChatHistoryPersistingChatClient
// detects the pending message before the next service call and includes the injected message in the request.
//
// To use end-of-run persistence instead (atomic run semantics), remove the
// RequirePerServiceCallChatHistoryPersistence = true setting (or set it to false). End-of-run
// persistence is the default behavior.
Expand Down Expand Up @@ -54,6 +59,36 @@ static string GetTime([Description("The city name.")] string city) =>
_ => $"{city}: time data not available."
};

// This tool demonstrates message injection during the function execution loop.
// When called, it checks travel advisories for a city. If an advisory is active, it uses
// the ambient run context to resolve IChatMessageInjector and injects a follow-up user message
// asking for alternative destinations. The model will process this injected message on the next
// service call — even though the parent FunctionInvokingChatClient loop would otherwise stop.
[Description("Check current travel advisories for a city.")]
static string CheckTravelAdvisory([Description("The city name.")] string city)
{
// Simulated travel advisory data.
var advisory = city.ToUpperInvariant() switch
{
"LONDON" => "Travel advisory: Severe fog warnings in London. Flights may be delayed or cancelled.",
"SEATTLE" => "Travel advisory: Heavy rainfall expected. Flooding possible in low-lying areas.",
_ => null
};

if (advisory is null)
{
return $"{city}: No active travel advisories.";
}

// When an advisory is found, inject a follow-up question so the model automatically
// suggests alternatives without the user needing to ask.
AIAgent.CurrentRunContext!.Agent.GetService<IChatMessageInjector>()
Comment thread
westey-m marked this conversation as resolved.
Outdated
?.EnqueueMessages([new ChatMessage(ChatRole.User,
$"Given the travel advisory for {city}, what alternative cities would you recommend instead?")]);

return advisory;
}

// Create the agent — per-service-call persistence is enabled via RequirePerServiceCallChatHistoryPersistence.
// The in-memory ChatHistoryProvider is used by default when the service does not require service stored chat
// history, so for those cases, we can inspect the chat history via session.TryGetInMemoryChatHistory().
Expand All @@ -67,8 +102,8 @@ static string GetTime([Description("The city name.")] string city) =>
RequirePerServiceCallChatHistoryPersistence = true,
ChatOptions = new()
{
Instructions = "You are a helpful assistant. When asked about multiple cities, call the appropriate tool for each city.",
Tools = [AIFunctionFactory.Create(GetWeather), AIFunctionFactory.Create(GetTime)]
Instructions = "You are a helpful travel assistant. When asked about cities, call the appropriate tools for each city.",
Tools = [AIFunctionFactory.Create(GetWeather), AIFunctionFactory.Create(GetTime), AIFunctionFactory.Create(CheckTravelAdvisory)]
},
});

Expand Down Expand Up @@ -109,6 +144,18 @@ async Task RunNonStreamingAsync()
response = await agent.RunAsync(FollowUp2, session);
PrintAgentResponse(response.Text);
PrintChatHistory(session, "After third run", ref lastChatHistorySize, ref lastConversationId);

// Fourth turn — demonstrates message injection during the function loop.
// The CheckTravelAdvisory tool detects an advisory for London and injects a follow-up
// user message asking for alternative cities. After the tool completes, the internal loop
// in PerServiceCallChatHistoryPersistingChatClient detects the pending injected message
// and calls the service again, so the model answers the follow-up automatically.
const string TravelPrompt = "I'm planning to travel to London next week. Check if there are any travel advisories.";
PrintUserMessage(TravelPrompt);

response = await agent.RunAsync(TravelPrompt, session);
PrintAgentResponse(response.Text);
PrintChatHistory(session, "After travel advisory run", ref lastChatHistorySize, ref lastConversationId);
}

async Task RunStreamingAsync()
Expand Down Expand Up @@ -181,6 +228,30 @@ async Task RunStreamingAsync()

Console.WriteLine();
PrintChatHistory(session, "After third run", ref lastChatHistorySize, ref lastConversationId);

// Fourth turn — demonstrates message injection during the function loop (streaming).
// The CheckTravelAdvisory tool detects an advisory for London and injects a follow-up
// user message asking for alternative cities. After the tool completes, the internal loop
// in PerServiceCallChatHistoryPersistingChatClient detects the pending injected message
// and calls the service again, so the model answers the follow-up automatically.
const string TravelPrompt = "I'm planning to travel to London next week. Check if there are any travel advisories.";
PrintUserMessage(TravelPrompt);

Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("\n[Agent] ");
Console.ResetColor();

await foreach (var update in agent.RunStreamingAsync(TravelPrompt, session))
{
Console.Write(update);

// During streaming we should be able to see updates to the chat history
// before the full run completes, as each service call is made and persisted.
PrintChatHistory(session, "During travel advisory run", ref lastChatHistorySize, ref lastConversationId);
}

Console.WriteLine();
PrintChatHistory(session, "After travel advisory run", ref lastChatHistorySize, ref lastConversationId);
}

void PrintUserMessage(string message)
Expand Down
26 changes: 26 additions & 0 deletions dotnet/src/Microsoft.Agents.AI/ChatClient/IChatMessageInjector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Agents.AI;

/// <summary>
/// Provides a mechanism for injecting messages into an agent run, while the agent is executing, where an agent supports this behavior.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public interface IChatMessageInjector
{
/// <summary>
/// Enqueues one or more messages to be used at the next opportunity.
/// </summary>
/// <remarks>
/// This method is thread-safe and can be called concurrently from tool delegates or other code
/// while the function execution loop is in progress. The enqueued messages will be picked up
/// at the next opportunity.
/// </remarks>
/// <param name="messages">The messages to enqueue.</param>
void EnqueueMessages(IEnumerable<ChatMessage> messages);
}
Loading
Loading