.NET: Add more console based getting started samples (#507)

* Add more console based getting started samples

* Simplify function calling and approavls samples and some minor renaming based on PR feedback.

* Cover streaming with comments for aprovals sample.

* Remove extra line break.

* Update getting started samples list in readme.

* Address PR comments

* Address PR comments.
This commit is contained in:
westey
2025-08-28 15:14:25 +01:00
committed by GitHub
Unverified
parent 6b22b6bbc7
commit bbea3c00f8
28 changed files with 634 additions and 900 deletions
@@ -1,187 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
namespace Steps;
/// <summary>
/// Provides test methods to demonstrate the usage of chat agents with different interaction models.
/// </summary>
/// <remarks>This class contains examples of using <see cref="ChatClientAgent"/> to showcase scenarios with and without conversation history.
/// Each test method demonstrates how to configure and interact with the agents, including handling user input and displaying responses.
/// </remarks>
public sealed class Step01_ChatClientAgent_Running(ITestOutputHelper output) : AgentSample(output)
{
private const string ParrotName = "Parrot";
private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound.";
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
/// <summary>
/// Demonstrate the most basic Agent case, where we do not have a server-side agent
/// but just an in-memory agent, backed by an inference service,
/// and we are invoking with text input, and getting back a text response.
/// </summary>
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task RunBasic(ChatClientProviders provider)
{
// Get the chat client to communicate with the inference service backing our agent.
// Any implementation of Microsoft.Extensions.AI.Agents.IChatClient can be used with the ChatClientAgent.
// See the Providers folder for examples on how to create chat clients for some sample providers.
IChatClient chatClient = base.GetChatClient(provider);
// Define the agent
AIAgent agent = new ChatClientAgent(chatClient, ParrotInstructions);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Fortune favors the bold."));
}
/// <summary>
/// Demonstrate the usage of <see cref="ChatClientAgent"/> where each invocation is
/// a unique interaction with no conversation history between them.
/// </summary>
[Theory]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task RunWithoutThread(ChatClientProviders provider)
{
// Define the options for the chat client agent.
var agentOptions = new ChatClientAgentOptions(name: ParrotName, instructions: ParrotInstructions);
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Respond to user input
await RunAgentAsync("Fortune favors the bold.");
await RunAgentAsync("I came, I saw, I conquered.");
await RunAgentAsync("Practice makes perfect.");
// Local function to invoke agent and display the conversation messages.
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var response = await agent.RunAsync(input);
this.WriteResponseOutput(response);
}
// Clean up the server-side agent after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent);
}
/// <summary>
/// Demonstrate the usage of <see cref="ChatClientAgent"/> where a conversation history is maintained.
/// </summary>
[Theory]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)]
[InlineData(ChatClientProviders.OpenAIResponses_ConversationIdThread)]
public async Task RunWithThread(ChatClientProviders provider)
{
// Define the options for the chat client agent.
var agentOptions = new ChatClientAgentOptions
{
Name = JokerName,
Instructions = JokerInstructions,
// Get chat options based on the store type, if needed.
ChatOptions = base.GetChatOptions(provider),
};
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to invoke agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var response = await agent.RunAsync(input, thread);
this.WriteResponseOutput(response);
}
// Clean up the server-side agent and thread after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
/// <summary>
/// Demonstrate the usage of <see cref="ChatClientAgent"/> in streaming mode,
/// where a conversation is maintained by the <see cref="AgentThread"/>.
/// </summary>
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)]
[InlineData(ChatClientProviders.OpenAIResponses_ConversationIdThread)]
public async Task RunStreamingWithThread(ChatClientProviders provider)
{
// Define the options for the chat client agent.
var agentOptions = new ChatClientAgentOptions(name: JokerName, instructions: JokerInstructions)
{
// Get chat options based on the store type, if needed.
ChatOptions = base.GetChatOptions(provider),
};
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to invoke agent and display the conversation messages.
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
await foreach (var update in agent.RunStreamingAsync(input, thread))
{
this.WriteAgentOutput(update);
}
}
// Clean up the server-side agent and thread after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
}
@@ -1,154 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
namespace Steps;
/// <summary>
/// This sample demonstrates how to use a <see cref="ChatClientAgent"/> with function tools.
/// It includes examples of both streaming and non-streaming agent interactions.
/// </summary>
public sealed class Step02_ChatClientAgent_UsingFunctionTools(ITestOutputHelper output) : AgentSample(output)
{
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task RunningWithTools(ChatClientProviders provider)
{
// Creating a MenuTools instance to be used by the agent.
var menuTools = new MenuTools();
// Define the options for the chat client agent.
var agentOptions = new ChatClientAgentOptions(
name: "Host",
instructions: "Answer questions about the menu",
tools: [
AIFunctionFactory.Create(menuTools.GetMenu),
AIFunctionFactory.Create(menuTools.GetSpecials),
AIFunctionFactory.Create(menuTools.GetItemPrice)
]);
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Create the chat history thread to capture the agent interaction.
var thread = agent.GetNewThread();
// Respond to user input, invoking functions where appropriate.
await RunAgentAsync("Hello");
await RunAgentAsync("What is the special soup and its price?");
await RunAgentAsync("What is the special drink and its price?");
await RunAgentAsync("Thank you");
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var response = await agent.RunAsync(input, thread);
this.WriteResponseOutput(response);
}
// Clean up the server-side agent after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task StreamingRunWithTools(ChatClientProviders provider)
{
// Creating a MenuTools instance to be used by the agent.
var menuTools = new MenuTools();
// Define the options for the chat client agent.
var agentOptions = new ChatClientAgentOptions(
name: "Host",
instructions: "Answer questions about the menu",
tools: [
AIFunctionFactory.Create(menuTools.GetMenu),
AIFunctionFactory.Create(menuTools.GetSpecials),
AIFunctionFactory.Create(menuTools.GetItemPrice)
]);
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Create the chat history thread to capture the agent interaction.
var thread = agent.GetNewThread();
// Respond to user input, invoking functions where appropriate.
await RunAgentAsync("Hello");
await RunAgentAsync("What is the special soup and its price?");
await RunAgentAsync("What is the special drink and its price?");
await RunAgentAsync("Thank you");
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
await foreach (var update in agent.RunStreamingAsync(input, thread))
{
this.WriteAgentOutput(update);
}
}
// Clean up the server-side agent after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
private sealed class MenuTools
{
[Description("Get the full menu items.")]
public MenuItem[] GetMenu()
{
return s_menuItems;
}
[Description("Get the specials from the menu.")]
public IEnumerable<MenuItem> GetSpecials()
{
return s_menuItems.Where(i => i.IsSpecial);
}
[Description("Get the price of a menu item.")]
public float? GetItemPrice([Description("The name of the menu item.")] string menuItem)
{
return s_menuItems.FirstOrDefault(i => i.Name.Equals(menuItem, StringComparison.OrdinalIgnoreCase))?.Price;
}
private static readonly MenuItem[] s_menuItems = [
new() { Category = "Soup", Name = "Clam Chowder", Price = 4.95f, IsSpecial = true },
new() { Category = "Soup", Name = "Tomato Soup", Price = 4.95f, IsSpecial = false },
new() { Category = "Salad", Name = "Cobb Salad", Price = 9.99f },
new() { Category = "Salad", Name = "House Salad", Price = 4.95f },
new() { Category = "Drink", Name = "Chai Tea", Price = 2.95f, IsSpecial = true },
new() { Category = "Drink", Name = "Soda", Price = 1.95f },
];
public sealed class MenuItem
{
public string Category { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public float Price { get; set; }
public bool IsSpecial { get; set; }
}
}
}
@@ -1,68 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Steps;
public sealed class Step04_ChatClientAgent_DependencyInjection(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
[Theory]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task RunningWithServiceCollection(ChatClientProviders provider)
{
// Adding multiple chat clients to the service collection.
var services = new ServiceCollection();
var agentOptions = new ChatClientAgentOptions(JokerInstructions, JokerName);
services.AddLogging();
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
services.AddSingleton(agentOptions);
services.AddChatClient((sp) => base.GetChatClient(provider, sp.GetRequiredService<ChatClientAgentOptions>()));
services.AddSingleton<AIAgent>((sp)
=> new ChatClientAgent(
chatClient: sp.GetRequiredService<IChatClient>(),
options: sp.GetRequiredService<ChatClientAgentOptions>(),
loggerFactory: sp.GetRequiredService<ILoggerFactory>()));
// Build the service provider.
await using var serviceProvider = services.BuildServiceProvider();
// Get the agent from the service provider.
var agent = serviceProvider.GetRequiredService<AIAgent>();
// Create the chat history thread to capture the agent interaction.
var thread = agent.GetNewThread();
Console.WriteLine($"Using chat client for provider: {provider}");
// Respond to user input, invoking functions where appropriate.
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var response = await agent.RunAsync(input, thread);
this.WriteResponseOutput(response);
}
// Clean up the agent and thread after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
}
@@ -1,56 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI.Agents;
using OpenTelemetry;
using OpenTelemetry.Trace;
namespace Steps;
/// <summary>
/// Demonstrates how to use telemetry with <see cref="ChatClientAgent"/> using OpenTelemetry.
/// </summary>
public sealed class Step05_ChatClientAgent_Telemetry(ITestOutputHelper output) : AgentSample(output)
{
/// <summary>
/// Demonstrates OpenTelemetry tracing with Agent Framework.
/// </summary>
[Theory]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task RunWithTelemetry(ChatClientProviders provider)
{
// Enable telemetry
AppContext.SetSwitch("Microsoft.Extensions.AI.Agents.EnableTelemetry", true);
// Create TracerProvider with console exporter
string sourceName = Guid.NewGuid().ToString();
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
.AddConsoleExporter()
.Build();
// Define agent options
var agentOptions = new ChatClientAgentOptions(name: "TelemetryAgent", instructions: "You are a helpful assistant.");
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
using var chatClient = base.GetChatClient(provider, agentOptions);
var baseAgent = new ChatClientAgent(chatClient, agentOptions);
// Wrap the agent with OpenTelemetry instrumentation
using var agent = baseAgent.WithOpenTelemetry(sourceName: sourceName);
var thread = agent.GetNewThread();
// Run agent interactions
await agent.RunAsync("What is artificial intelligence?", thread);
await agent.RunAsync("How does machine learning work?", thread);
// Clean up
await base.AgentCleanUpAsync(provider, baseAgent, thread);
}
}
@@ -1,77 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
namespace Steps;
/// <summary>
/// Demonstrates how to use structured outputs with <see cref="ChatClientAgent"/>.
/// </summary>
public sealed class Step06_ChatClientAgent_StructuredOutputs(ITestOutputHelper output) : AgentSample(output)
{
/// <summary>
/// Demonstrates processing structured outputs using JSON schemas to extract information about a person.
/// </summary>
[Theory]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task RunWithCustomSchema(ChatClientProviders provider)
{
var agentOptions = new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant.")
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(PersonInfo)),
schemaName: "PersonInfo",
schemaDescription: "Information about a person including their name, age, and occupation"
)
}
};
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
using var chatClient = base.GetChatClient(provider, agentOptions);
ChatClientAgent agent = new(chatClient, agentOptions);
var thread = agent.GetNewThread();
const string Prompt = "Please provide information about John Smith, who is a 35-year-old software engineer.";
var updates = agent.RunStreamingAsync(Prompt, thread);
var agentResponse = await updates.ToAgentRunResponseAsync();
var personInfo = agentResponse.Deserialize<PersonInfo>(JsonSerializerOptions.Web);
Console.WriteLine("Assistant Output:");
Console.WriteLine($"Name: {personInfo.Name}");
Console.WriteLine($"Age: {personInfo.Age}");
Console.WriteLine($"Occupation: {personInfo.Occupation}");
// Clean up the server-side agent after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
/// <summary>
/// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent.
/// </summary>
public class PersonInfo
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("age")]
public int? Age { get; set; }
[JsonPropertyName("occupation")]
public string? Occupation { get; set; }
}
}
@@ -1,68 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Extensions.AI.Agents;
namespace Steps;
/// <summary>
/// Demonstrates how to suspend and resume a thread with the <see cref="ChatClientAgent"/>.
/// </summary>
public sealed class Step08_ChatClientAgent_SuspendResumeThread(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
/// <summary>
/// Demonstrate the usage of <see cref="ChatClientAgent"/> where a thread is suspended.
/// The thread is serialized and can be stored to a database, file, or any other storage mechanism,
/// and then deserialized later to resume the conversation with the agent.
/// </summary>
[Theory]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)]
[InlineData(ChatClientProviders.OpenAIResponses_ConversationIdThread)]
public async Task SuspendResumeThread(ChatClientProviders provider)
{
// Define the options for the chat client agent.
var agentOptions = new ChatClientAgentOptions
{
Name = JokerName,
Instructions = JokerInstructions,
// Get chat options based on the store type, if needed.
ChatOptions = base.GetChatOptions(provider),
};
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Serialize the thread state, so it can be stored for later use.
JsonElement serializedThread = await thread.SerializeAsync();
// The thread can now be saved to a database, file, or any other storage mechanism
// and loaded again later.
// Deserialize the thread state after loading from storage.
AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread);
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
// Clean up the server-side agent and thread after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
}
@@ -1,152 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
namespace Steps;
/// <summary>
/// Demonstrates how to store the chat history of a thread in a 3rd party store when using <see cref="ChatClientAgent"/>.
/// </summary>
public sealed class Step09_ChatClientAgent_3rdPartyThreadStorage(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
/// <summary>
/// Demonstrate storage of the chat history of a thread in a 3rd party store when using <see cref="ChatClientAgent"/>.
/// </summary>
/// <remarks>
/// Note that this is only supported for services that do not already store the chat history in their own service.
/// </remarks>
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIResponses_InMemoryMessageThread)]
public async Task ThirdPartyStorageThread(ChatClientProviders provider)
{
VectorStore vectorStore = new InMemoryVectorStore();
// Define the options for the chat client agent.
var agentOptions = new ChatClientAgentOptions
{
Name = JokerName,
Instructions = JokerInstructions,
ChatMessageStoreFactory = () =>
{
// 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
// also contains the id that the thread is stored under.
return new VectorChatMessageStore(vectorStore);
}
};
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Serialize the thread state, so it can be stored for later use.
// Since the chat history is stored in the vector store, the serialized there
// only contains the guid that the messages are stored under in the vector store.
JsonElement serializedThread = await thread.SerializeAsync();
// The serialized thread can now be saved to a database, file, or any other storage mechanism
// and loaded again later.
// Deserialize the thread state after loading from storage.
AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread);
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
}
/// <summary>
/// A sample implementation of <see cref="IChatMessageStore"/> that stores chat messages in a vector store.
/// </summary>
/// <param name="vectorStore">The vector store to store the messages in.</param>
private sealed class VectorChatMessageStore(VectorStore vectorStore) : IChatMessageStore
{
private string? _threadId;
public string? ThreadId => this._threadId;
public async Task AddMessagesAsync(IReadOnlyCollection<ChatMessage> messages, CancellationToken cancellationToken)
{
this._threadId ??= Guid.NewGuid().ToString();
var collection = vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
await collection.UpsertAsync(messages.Select(x => new ChatHistoryItem()
{
Key = this._threadId + x.MessageId,
Timestamp = DateTimeOffset.UtcNow,
ThreadId = this._threadId,
SerializedMessage = JsonSerializer.Serialize(x),
MessageText = x.Text
}), cancellationToken);
}
public async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken)
{
var collection = vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
var records = await collection
.GetAsync(
x => x.ThreadId == this._threadId, 10,
new() { OrderBy = x => x.Descending(y => y.Timestamp) },
cancellationToken)
.ToListAsync(cancellationToken);
var messages = records
.Select(x => JsonSerializer.Deserialize<ChatMessage>(x.SerializedMessage!)!)
.ToList();
messages.Reverse();
return messages;
}
public ValueTask<JsonElement?> SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
// We have to serialize the thread id, so that on deserialization we can retrieve the messages using the same thread id.
return new ValueTask<JsonElement?>(JsonSerializer.SerializeToElement(this._threadId));
}
public ValueTask DeserializeStateAsync(JsonElement? serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
// Here we can deserialize the thread id so that we can access the same messages as before the suspension.
this._threadId = JsonSerializer.Deserialize<string>((JsonElement)serializedStoreState!);
return new ValueTask();
}
/// <summary>
/// The data structure used to store chat history items in the vector store.
/// </summary>
private sealed class ChatHistoryItem
{
[VectorStoreKey]
public string? Key { get; set; }
[VectorStoreData]
public string? ThreadId { get; set; }
[VectorStoreData]
public DateTimeOffset? Timestamp { get; set; }
[VectorStoreData]
public string? SerializedMessage { get; set; }
[VectorStoreData]
public string? MessageText { get; set; }
}
}
}
@@ -1,205 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
namespace Steps;
/// <summary>
/// Demonstrates how to indicate that certain function calls require user approval before they can be executed and how to then approve or reject those function calls.
/// </summary>
public sealed class Step10_ChatClientAgent_UsingFunctionToolsWithApprovals(ITestOutputHelper output) : AgentSample(output)
{
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task ApprovalsWithTools(ChatClientProviders provider)
{
// Creating a MenuTools instance to be used by the agent.
var menuTools = new MenuTools();
// Define the options for the chat client agent.
// We mark GetMenu and GetSpecial as requiring approval before they can be invoked, while GetItemPrice can be invoked without user approval.
// IMPORTANT: A limitation of the approvals flow when using ChatClientAgent is that if more than one function needs to be executed in one run,
// and any one of them requires approval, approval will be sought for all function calls produced during that run.
var agentOptions = new ChatClientAgentOptions(
name: "Host",
instructions: "Answer questions about the menu",
tools: [
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetMenu)),
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetSpecials)),
AIFunctionFactory.Create(menuTools.GetItemPrice)
]);
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Create the chat history thread to capture the agent interaction.
var thread = agent.GetNewThread();
// Respond to user input, invoking functions where appropriate.
await RunAgentAsync("What is the special soup and its price?");
await RunAgentAsync("What is the special drink?");
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var response = await agent.RunAsync(input, thread);
// Loop until all user input requests are handled.
var userInputRequests = response.UserInputRequests.ToList();
while (userInputRequests.Count > 0)
{
// Approve GetSpecials function calls, reject all others.
List<ChatMessage> nextIterationMessages = userInputRequests?.Select((request) => request switch
{
FunctionApprovalRequestContent functionApprovalRequest when functionApprovalRequest.FunctionCall.Name == "GetSpecials" =>
new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]),
FunctionApprovalRequestContent functionApprovalRequest =>
new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: false)]),
_ => throw new NotSupportedException($"Unsupported user input request type: {request.GetType().Name}")
})?.ToList() ?? [];
// Write out what the decision was for each function approval request.
nextIterationMessages.ForEach(x => Console.WriteLine($"Approval for the {(x.Contents[0] as FunctionApprovalResponseContent)?.FunctionCall.Name} function call is set to {(x.Contents[0] as FunctionApprovalResponseContent)?.Approved}."));
// Pass the user input responses back to the agent for further processing.
response = await agent.RunAsync(nextIterationMessages, thread);
userInputRequests = response.UserInputRequests.ToList();
}
this.WriteResponseOutput(response);
}
// Clean up the server-side agent after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
[InlineData(ChatClientProviders.OpenAIAssistant)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
[InlineData(ChatClientProviders.OpenAIResponses)]
public async Task ApprovalsWithToolsStreaming(ChatClientProviders provider)
{
// Creating a MenuTools instance to be used by the agent.
var menuTools = new MenuTools();
// Creating a MenuTools instance to be used by the agent.
// We mark GetMenu and GetSpecial as requiring approval before they can be invoked, while GetItemPrice can be invoked without user approval.
// IMPORTANT: A limitation of the approvals flow when using ChatClientAgent is that if more than one function needs to be executed in one run,
// and any one of them requires approval, approval will be sought for all function calls produced during that run.
var agentOptions = new ChatClientAgentOptions(
name: "Host",
instructions: "Answer questions about the menu",
tools: [
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetMenu)),
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetSpecials)),
AIFunctionFactory.Create(menuTools.GetItemPrice),
]);
// Create the server-side agent Id when applicable (depending on the provider).
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);
// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
// Create the chat history thread to capture the agent interaction.
var thread = agent.GetNewThread();
// Respond to user input, invoking functions where appropriate.
await RunAgentAsync("What is the special soup and its price?");
await RunAgentAsync("What is the special drink?");
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var updates = await agent.RunStreamingAsync(input, thread).ToListAsync();
// Loop until all user input requests are handled.
var userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
while (userInputRequests.Count > 0)
{
// Approve GetSpecials function calls, reject all others.
List<ChatMessage> nextIterationMessages = userInputRequests?.Select((request) => request switch
{
FunctionApprovalRequestContent functionApprovalRequest when functionApprovalRequest.FunctionCall.Name == "GetSpecials" =>
new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]),
FunctionApprovalRequestContent functionApprovalRequest =>
new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: false)]),
_ => throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}")
})?.ToList() ?? [];
// Write out what the decision was for each function approval request.
nextIterationMessages.ForEach(x => Console.WriteLine($"Approval for the {(x.Contents[0] as FunctionApprovalResponseContent)?.FunctionCall.Name} function call is set to {(x.Contents[0] as FunctionApprovalResponseContent)?.Approved}."));
// Pass the user input responses back to the agent for further processing.
updates = await agent.RunStreamingAsync(nextIterationMessages, thread).ToListAsync();
userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
}
this.WriteResponseOutput(updates.ToAgentRunResponse());
}
// Clean up the server-side agent after use when applicable (depending on the provider).
await base.AgentCleanUpAsync(provider, agent, thread);
}
private sealed class MenuTools
{
[Description("Get the full menu items.")]
public MenuItem[] GetMenu()
{
return s_menuItems;
}
[Description("Get the specials from the menu.")]
public IEnumerable<MenuItem> GetSpecials()
{
return s_menuItems.Where(i => i.IsSpecial);
}
[Description("Get the price of a menu item.")]
public float? GetItemPrice([Description("The name of the menu item.")] string menuItem)
{
return s_menuItems.FirstOrDefault(i => i.Name.Equals(menuItem, StringComparison.OrdinalIgnoreCase))?.Price;
}
private static readonly MenuItem[] s_menuItems = [
new() { Category = "Soup", Name = "Clam Chowder", Price = 4.95f, IsSpecial = true },
new() { Category = "Soup", Name = "Tomato Soup", Price = 4.95f, IsSpecial = false },
new() { Category = "Salad", Name = "Cobb Salad", Price = 9.99f },
new() { Category = "Salad", Name = "House Salad", Price = 4.95f },
new() { Category = "Drink", Name = "Chai Tea", Price = 2.95f, IsSpecial = true },
new() { Category = "Drink", Name = "Soda", Price = 1.95f },
];
public sealed class MenuItem
{
public string Category { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public float Price { get; set; }
public bool IsSpecial { get; set; }
}
}
}